diff --git a/.eslintrc.json b/.eslintrc.json index af1b97849b6..50a9be3d59b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,10 @@ "eslint-plugin-deprecation", "unused-imports", "eslint-plugin-lodash", - "eslint-plugin-jsonc" + "eslint-plugin-jsonc", + "eslint-plugin-rxjs", + "eslint-plugin-simple-import-sort", + "eslint-plugin-import-newlines" ], "overrides": [ { @@ -27,17 +30,29 @@ "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:rxjs/recommended" ], "rules": { + "indent": [ + "error", + 2, + { + "SwitchCase": 1 + } + ], "max-classes-per-file": [ "error", 1 ], "comma-dangle": [ - "off", + "error", "always-multiline" ], + "object-curly-spacing": [ + "error", + "always" + ], "eol-last": [ "error", "always" @@ -104,15 +119,13 @@ "allowTernary": true } ], - "prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint) + "prefer-const": "error", + "no-case-declarations": "error", + "no-extra-boolean-cast": "error", "prefer-spread": "off", "no-underscore-dangle": "off", - - // todo: disabled rules from eslint:recommended, consider re-enabling & fixing "no-prototype-builtins": "off", "no-useless-escape": "off", - "no-case-declarations": "off", - "no-extra-boolean-cast": "off", "@angular-eslint/directive-selector": [ "error", @@ -139,7 +152,6 @@ } ], "@angular-eslint/no-attribute-decorator": "error", - "@angular-eslint/no-forward-ref": "error", "@angular-eslint/no-output-native": "warn", "@angular-eslint/no-output-on-prefix": "warn", "@angular-eslint/no-conflicting-lifecycle": "warn", @@ -183,7 +195,7 @@ ], "@typescript-eslint/type-annotation-spacing": "error", "@typescript-eslint/unified-signatures": "error", - "@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable + "@typescript-eslint/ban-types": "error", "@typescript-eslint/no-floating-promises": "warn", "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/restrict-plus-operands": "warn", @@ -203,14 +215,45 @@ "deprecation/deprecation": "warn", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", "import/order": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", "import/no-deprecated": "warn", "import/no-namespace": "error", + "import-newlines/enforce": [ + "error", + { + "items": 1, + "semi": true, + "forceSingleLine": true + } + ], + "unused-imports/no-unused-imports": "error", "lodash/import-scope": [ "error", "method" - ] + ], + + "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + } + }, + { + "files": [ + "*.spec.ts" + ], + "parserOptions": { + "project": [ + "./tsconfig.json", + "./cypress/tsconfig.json" + ], + "createDefaultProgram": true + }, + "rules": { + "prefer-const": "off" } }, { @@ -219,12 +262,7 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ], - "rules": { - // todo: re-enable & fix errors - "@angular-eslint/template/no-negated-async": "off", - "@angular-eslint/template/eqeqeq": "off" - } + ] }, { "files": [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3c..f6ffa5e004b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,12 +33,12 @@ jobs: #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' - # Project name to use when running docker-compose prior to e2e tests + # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' strategy: # Create a matrix of Node versions to test against (in parallel) matrix: - node-version: [16.x, 18.x] + node-version: [18.x, 20.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job @@ -74,7 +74,7 @@ jobs: id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Yarn dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: # Cache entire Yarn cache directory (see previous step) path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -101,19 +101,19 @@ jobs: # 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 - name: Upload code coverage report to Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: matrix.node-version == '18.x' with: - name: dspace-angular coverage report + name: coverage-report-${{ matrix.node-version }} path: 'coverage/dspace-angular/lcov.info' retention-days: 14 - # Using docker-compose start backend using CI configuration + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) run: | - docker-compose -f ./docker/docker-compose-ci.yml up -d - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker compose -f ./docker/docker-compose-ci.yml up -d + docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls # Run integration tests via Cypress.io @@ -135,19 +135,19 @@ jobs: # Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Save those in an Artifact - name: Upload e2e test videos to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: - name: e2e-test-videos + name: e2e-test-videos-${{ matrix.node-version }} path: cypress/videos # If e2e tests fail, Cypress creates a screenshot of what happened # Save those in an Artifact - name: Upload e2e test failure screenshots to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: - name: e2e-test-screenshots + name: e2e-test-screenshots-${{ matrix.node-version }} path: cypress/screenshots - name: Stop app (in case it stays up after e2e tests) @@ -182,7 +182,7 @@ jobs: run: kill -9 $(lsof -t -i:4000) - name: Shutdown Docker containers - run: docker-compose -f ./docker/docker-compose-ci.yml down + 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. @@ -197,7 +197,7 @@ jobs: # Download artifacts from previous 'tests' job - name: Download coverage artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 # 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. @@ -207,11 +207,12 @@ jobs: - name: Upload coverage to Codecov.io uses: Wandalen/wretry.action@v1.3.0 with: - action: codecov/codecov-action@v3 + action: codecov/codecov-action@v4 # Ensure codecov-action throws an error when it fails to upload # This allows us to auto-restart the action if an error is thrown with: | fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} # Try re-running action 5 times max attempt_limit: 5 # Run again in 30 seconds diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 85a72161131..d0b4cd0939f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main with: - build_id: dspace-angular + build_id: dspace-angular-dev image_name: dspace/dspace-angular dockerfile_path: ./Dockerfile secrets: diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index b4436dca3aa..0a35a6a9504 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.5.0 + uses: actions/add-to-project@v1.0.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/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index f16e81c9fd2..bbac52af243 100644 --- a/.github/workflows/pull_request_opened.yml +++ b/.github/workflows/pull_request_opened.yml @@ -21,4 +21,4 @@ jobs: # 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@v2.0.1 + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.gitignore b/.gitignore index 7d065aca061..ce44f6b3fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.angular/cache +/.nx /__build__ /__server_build__ /node_modules diff --git a/angular.json b/angular.json index 5e597d4d307..98463fa732e 100644 --- a/angular.json +++ b/angular.json @@ -109,22 +109,22 @@ "serve": { "builder": "@angular-builders/custom-webpack:dev-server", "options": { - "browserTarget": "dspace-angular:build", + "buildTarget": "dspace-angular:build", "port": 4000 }, "configurations": { "development": { - "browserTarget": "dspace-angular:build:development" + "buildTarget": "dspace-angular:build:development" }, "production": { - "browserTarget": "dspace-angular:build:production" + "buildTarget": "dspace-angular:build:production" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "dspace-angular:build" + "buildTarget": "dspace-angular:build" } }, "test": { @@ -217,23 +217,23 @@ } }, "serve-ssr": { - "builder": "@nguniversal/builders:ssr-dev-server", + "builder": "@angular-devkit/build-angular:ssr-dev-server", "options": { - "browserTarget": "dspace-angular:build", + "buildTarget": "dspace-angular:build", "serverTarget": "dspace-angular:server", "port": 4000 }, "configurations": { "production": { - "browserTarget": "dspace-angular:build:production", + "buildTarget": "dspace-angular:build:production", "serverTarget": "dspace-angular:server:production" } } }, "prerender": { - "builder": "@nguniversal/builders:prerender", + "builder": "@angular-devkit/build-angular:prerender", "options": { - "browserTarget": "dspace-angular:build:production", + "buildTarget": "dspace-angular:build:production", "serverTarget": "dspace-angular:server:production", "routes": [ "/" diff --git a/config/config.example.yml b/config/config.example.yml index 8b010ba6ea6..82c061dab22 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -17,6 +17,13 @@ ui: # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true +universal: + # Whether to inline "critical" styles into the server-side rendered HTML. + # Determining which styles are critical is a relatively expensive operation; + # this option can be disabled to boost server performance at the expense of + # loading smoothness. + inlineCriticalCss: true + # The REST API server settings # NOTE: these settings define which (publicly available) REST API to use. They are usually # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. @@ -131,6 +138,10 @@ submission: # NOTE: after how many time (milliseconds) submission is saved automatically # eg. timer: 5 * (1000 * 60); // 5 minutes timer: 0 + # Always show the duplicate detection section if enabled, even if there are no potential duplicates detected + # (a message will be displayed to indicate no matches were found) + duplicateDetection: + alwaysShowSection: false icons: metadata: # NOTE: example of configuration @@ -396,10 +407,11 @@ mediaViewer: # Whether the end user agreement is required before users use the repository. # If enabled, the user will be required to accept the agreement before they can use the repository. -# And whether the privacy statement should exist or not. +# And whether the privacy statement/COAR notify support page should exist or not. info: enableEndUserAgreement: true enablePrivacyStatement: true + enableCOARNotifySupport: true # Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) # display in supported metadata fields. By default, only dc.description.abstract is supported. @@ -427,9 +439,67 @@ comcolSelectionSort: # Search settings -search: +search: # Settings to enable/disable or configure advanced search filters. advancedFilters: enabled: false # List of filters to enable in "Advanced Search" dropdown filter: [ 'title', 'author', 'subject', 'entityType' ] + + +# Notify metrics +# Configuration for Notify Admin Dashboard for metrics visualization +notifyMetrics: + # Configuration for received messages +- title: 'admin-notify-dashboard.received-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.incoming.accepted' + config: 'NOTIFY.incoming.accepted' + description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.incoming.processed' + config: 'NOTIFY.incoming.processed' + description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.failure' + config: 'NOTIFY.incoming.failure' + description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted' + config: 'NOTIFY.incoming.untrusted' + description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems' + textColor: '#fff' + config: 'NOTIFY.incoming.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description' +# Configuration for outgoing messages +- title: 'admin-notify-dashboard.generated-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued' + config: 'NOTIFY.outgoing.queued' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description' + - color: '#FDEEBB' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry' + config: 'NOTIFY.outgoing.queued_for_retry' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.outgoing.failure' + config: 'NOTIFY.outgoing.failure' + description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems' + textColor: '#fff' + config: 'NOTIFY.outgoing.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered' + config: 'NOTIFY.outgoing.delivered' + description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description' + + + + + diff --git a/cypress/e2e/admin-sidebar.cy.ts b/cypress/e2e/admin-sidebar.cy.ts index 7612eb53132..be1c9d4ef27 100644 --- a/cypress/e2e/admin-sidebar.cy.ts +++ b/cypress/e2e/admin-sidebar.cy.ts @@ -1,28 +1,28 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; describe('Admin Sidebar', () => { - beforeEach(() => { - // Must login as an Admin for sidebar to appear - cy.visit('/login'); - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - }); + beforeEach(() => { + // Must login as an Admin for sidebar to appear + cy.visit('/login'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); - it('should be pinnable and pass accessibility tests', () => { - // Pin the sidebar open - cy.get('#sidebar-collapse-toggle').click(); + it('should be pinnable and pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').click(); - // Click on every expandable section to open all menus - cy.get('ds-expandable-admin-sidebar-section').click({multiple: true}); + // Click on every expandable section to open all menus + cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true }); - // Analyze for accessibility - testA11y('ds-admin-sidebar', + // Analyze for accessibility + testA11y('ds-admin-sidebar', { - rules: { - // Currently all expandable sections have nested interactive elements - // See https://github.com/DSpace/dspace-angular/issues/2178 - 'nested-interactive': { enabled: false }, - } + rules: { + // Currently all expandable sections have nested interactive elements + // See https://github.com/DSpace/dspace-angular/issues/2178 + 'nested-interactive': { enabled: false }, + }, } as Options); - }); + }); }); diff --git a/cypress/e2e/breadcrumbs.cy.ts b/cypress/e2e/breadcrumbs.cy.ts index 0cddbc723c6..f660f47a540 100644 --- a/cypress/e2e/breadcrumbs.cy.ts +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -1,14 +1,14 @@ 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/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); + it('should pass accessibility tests', () => { + // Visit an Item, as those have more breadcrumbs + cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); - // Wait for breadcrumbs to be visible - cy.get('ds-breadcrumbs').should('be.visible'); + // Wait for breadcrumbs to be visible + cy.get('ds-breadcrumbs').should('be.visible'); - // Analyze for accessibility - testA11y('ds-breadcrumbs'); - }); + // Analyze for accessibility + testA11y('ds-breadcrumbs'); + }); }); diff --git a/cypress/e2e/browse-by-author.cy.ts b/cypress/e2e/browse-by-author.cy.ts index 32c470231d7..3e914a2f8c0 100644 --- a/cypress/e2e/browse-by-author.cy.ts +++ b/cypress/e2e/browse-by-author.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Browse By Author', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/author'); + it('should pass accessibility tests', () => { + cy.visit('/browse/author'); - // Wait for to be visible - cy.get('ds-browse-by-metadata').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-metadata').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-metadata'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-metadata'); + }); }); diff --git a/cypress/e2e/browse-by-dateissued.cy.ts b/cypress/e2e/browse-by-dateissued.cy.ts index 7966f1c82eb..5fe05433153 100644 --- a/cypress/e2e/browse-by-dateissued.cy.ts +++ b/cypress/e2e/browse-by-dateissued.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Browse By Date Issued', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/dateissued'); + it('should pass accessibility tests', () => { + cy.visit('/browse/dateissued'); - // Wait for to be visible - cy.get('ds-browse-by-date').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-date').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-date'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-date'); + }); }); diff --git a/cypress/e2e/browse-by-subject.cy.ts b/cypress/e2e/browse-by-subject.cy.ts index 57ca88d1032..0937a2542bb 100644 --- a/cypress/e2e/browse-by-subject.cy.ts +++ b/cypress/e2e/browse-by-subject.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Browse By Subject', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/subject'); + it('should pass accessibility tests', () => { + cy.visit('/browse/subject'); - // Wait for to be visible - cy.get('ds-browse-by-metadata').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-metadata').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-metadata'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-metadata'); + }); }); diff --git a/cypress/e2e/browse-by-title.cy.ts b/cypress/e2e/browse-by-title.cy.ts index 09195c30df2..71a7356ce32 100644 --- a/cypress/e2e/browse-by-title.cy.ts +++ b/cypress/e2e/browse-by-title.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Browse By Title', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/title'); + it('should pass accessibility tests', () => { + cy.visit('/browse/title'); - // Wait for to be visible - cy.get('ds-browse-by-title').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-title').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-title'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-title'); + }); }); diff --git a/cypress/e2e/collection-edit.cy.ts b/cypress/e2e/collection-edit.cy.ts index 63d873db3ec..e1ba1c5eed8 100644 --- a/cypress/e2e/collection-edit.cy.ts +++ b/cypress/e2e/collection-edit.cy.ts @@ -3,126 +3,126 @@ import { testA11y } from 'cypress/support/utils'; const COLLECTION_EDIT_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/edit'); beforeEach(() => { - // All tests start with visiting the Edit Collection Page - cy.visit(COLLECTION_EDIT_PAGE); + // All tests start with visiting the Edit Collection Page + cy.visit(COLLECTION_EDIT_PAGE); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); }); describe('Edit Collection > Edit Metadata tab', () => { - it('should pass accessibility tests', () => { - // tag must be loaded - cy.get('ds-edit-collection').should('be.visible'); + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-collection').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-edit-collection'); - }); + // Analyze for accessibility issues + testA11y('ds-edit-collection'); + }); }); describe('Edit Collection > Assign Roles tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="roles"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="roles"]').click(); - // tag must be loaded - cy.get('ds-collection-roles').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-roles').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-roles'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-roles'); + }); }); describe('Edit Collection > Content Source tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="source"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="source"]').click(); - // tag must be loaded - cy.get('ds-collection-source').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-source').should('be.visible'); - // Check the external source checkbox (to display all fields on the page) - cy.get('#externalSourceCheck').check(); + // Check the external source checkbox (to display all fields on the page) + cy.get('#externalSourceCheck').check(); - // Wait for the source controls to appear - // cy.get('ds-collection-source-controls').should('be.visible'); + // Wait for the source controls to appear + // cy.get('ds-collection-source-controls').should('be.visible'); - // Analyze entire page for accessibility issues - testA11y('ds-collection-source'); - }); + // Analyze entire page for accessibility issues + testA11y('ds-collection-source'); + }); }); describe('Edit Collection > Curate tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="curate"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); - // tag must be loaded - cy.get('ds-collection-curate').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-curate').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-curate'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-curate'); + }); }); describe('Edit Collection > Access Control tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="access-control"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').click(); - // tag must be loaded - cy.get('ds-collection-access-control').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-access-control').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-access-control'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-access-control'); + }); }); describe('Edit Collection > Authorizations tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="authorizations"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="authorizations"]').click(); - // tag must be loaded - cy.get('ds-collection-authorizations').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-authorizations').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-authorizations'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-authorizations'); + }); }); describe('Edit Collection > Item Mapper tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="mapper"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="mapper"]').click(); - // tag must be loaded - cy.get('ds-collection-item-mapper').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-item-mapper').should('be.visible'); - // Analyze entire page for accessibility issues - testA11y('ds-collection-item-mapper'); + // Analyze entire page for accessibility issues + testA11y('ds-collection-item-mapper'); - // Click on the "Map new Items" tab - cy.get('li[data-test="mapTab"] a').click(); + // Click on the "Map new Items" tab + cy.get('li[data-test="mapTab"] a').click(); - // Make sure search form is now visible - cy.get('ds-search-form').should('be.visible'); + // Make sure search form is now visible + cy.get('ds-search-form').should('be.visible'); - // Analyze entire page (again) for accessibility issues - testA11y('ds-collection-item-mapper'); - }); + // Analyze entire page (again) for accessibility issues + testA11y('ds-collection-item-mapper'); + }); }); describe('Edit Collection > Delete page', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="delete-button"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="delete-button"]').click(); - // tag must be loaded - cy.get('ds-delete-collection').should('be.visible'); + // tag must be loaded + cy.get('ds-delete-collection').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-delete-collection'); - }); + // Analyze for accessibility issues + testA11y('ds-delete-collection'); + }); }); diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index 55c10cc6e22..d12536d332a 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Collection Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + it('should pass accessibility tests', () => { + cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); - // tag must be loaded - cy.get('ds-collection-page').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-page'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-page'); + }); }); diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts index a08f8cb1987..3e5a465e398 100644 --- a/cypress/e2e/collection-statistics.cy.ts +++ b/cypress/e2e/collection-statistics.cy.ts @@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Collection Statistics Page', () => { - const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); - - it('should load if you click on "Statistics" from a Collection page', () => { - cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); - cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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(Cypress.env('DSPACE_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'); - }); + const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); + + it('should load if you click on "Statistics" from a Collection page', () => { + cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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(Cypress.env('DSPACE_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-edit.cy.ts b/cypress/e2e/community-edit.cy.ts index 8fc1a7733e7..77e260feec0 100644 --- a/cypress/e2e/community-edit.cy.ts +++ b/cypress/e2e/community-edit.cy.ts @@ -3,84 +3,84 @@ import { testA11y } from 'cypress/support/utils'; const COMMUNITY_EDIT_PAGE = '/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('/edit'); beforeEach(() => { - // All tests start with visiting the Edit Community Page - cy.visit(COMMUNITY_EDIT_PAGE); + // All tests start with visiting the Edit Community Page + cy.visit(COMMUNITY_EDIT_PAGE); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); }); describe('Edit Community > Edit Metadata tab', () => { - it('should pass accessibility tests', () => { - // tag must be loaded - cy.get('ds-edit-community').should('be.visible'); + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-community').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-edit-community'); - }); + // Analyze for accessibility issues + testA11y('ds-edit-community'); + }); }); describe('Edit Community > Assign Roles tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="roles"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="roles"]').click(); - // tag must be loaded - cy.get('ds-community-roles').should('be.visible'); + // tag must be loaded + cy.get('ds-community-roles').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-roles'); - }); + // Analyze for accessibility issues + testA11y('ds-community-roles'); + }); }); describe('Edit Community > Curate tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="curate"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); - // tag must be loaded - cy.get('ds-community-curate').should('be.visible'); + // tag must be loaded + cy.get('ds-community-curate').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-curate'); - }); + // Analyze for accessibility issues + testA11y('ds-community-curate'); + }); }); describe('Edit Community > Access Control tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="access-control"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').click(); - // tag must be loaded - cy.get('ds-community-access-control').should('be.visible'); + // tag must be loaded + cy.get('ds-community-access-control').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-access-control'); - }); + // Analyze for accessibility issues + testA11y('ds-community-access-control'); + }); }); describe('Edit Community > Authorizations tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="authorizations"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="authorizations"]').click(); - // tag must be loaded - cy.get('ds-community-authorizations').should('be.visible'); + // tag must be loaded + cy.get('ds-community-authorizations').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-authorizations'); - }); + // Analyze for accessibility issues + testA11y('ds-community-authorizations'); + }); }); describe('Edit Community > Delete page', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="delete-button"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="delete-button"]').click(); - // tag must be loaded - cy.get('ds-delete-community').should('be.visible'); + // tag must be loaded + cy.get('ds-delete-community').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-delete-community'); - }); + // Analyze for accessibility issues + testA11y('ds-delete-community'); + }); }); diff --git a/cypress/e2e/community-list.cy.ts b/cypress/e2e/community-list.cy.ts index c371f6ceae7..9b9c87b112d 100644 --- a/cypress/e2e/community-list.cy.ts +++ b/cypress/e2e/community-list.cy.ts @@ -2,16 +2,16 @@ import { testA11y } from 'cypress/support/utils'; describe('Community List Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/community-list'); + it('should pass accessibility tests', () => { + cy.visit('/community-list'); - // tag must be loaded - cy.get('ds-community-list-page').should('be.visible'); + // 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 }); + // 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'); - }); + // Analyze for accessibility issues + testA11y('ds-community-list-page'); + }); }); diff --git a/cypress/e2e/community-page.cy.ts b/cypress/e2e/community-page.cy.ts index 386bb592a0a..5a4441dbae8 100644 --- a/cypress/e2e/community-page.cy.ts +++ b/cypress/e2e/community-page.cy.ts @@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Community Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); + it('should pass accessibility tests', () => { + cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); - // tag must be loaded - cy.get('ds-community-page').should('be.visible'); + // tag must be loaded + cy.get('ds-community-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-page'); - }); + // Analyze for accessibility issues + testA11y('ds-community-page'); + }); }); diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts index 6cafed0350e..00e23a90b37 100644 --- a/cypress/e2e/community-statistics.cy.ts +++ b/cypress/e2e/community-statistics.cy.ts @@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Community Statistics Page', () => { - const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')); - - it('should load if you click on "Statistics" from a Community page', () => { - cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); - cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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(Cypress.env('DSPACE_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'); - }); + const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')); + + it('should load if you click on "Statistics" from a Community page', () => { + cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); + cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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(Cypress.env('DSPACE_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/e2e/footer.cy.ts b/cypress/e2e/footer.cy.ts index 656e9d47012..4ee1d6669ae 100644 --- a/cypress/e2e/footer.cy.ts +++ b/cypress/e2e/footer.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Footer', () => { - it('should pass accessibility tests', () => { - cy.visit('/'); + it('should pass accessibility tests', () => { + cy.visit('/'); - // Footer must first be visible - cy.get('ds-footer').should('be.visible'); + // Footer must first be visible + cy.get('ds-footer').should('be.visible'); - // Analyze for accessibility - testA11y('ds-footer'); - }); + // Analyze for accessibility + testA11y('ds-footer'); + }); }); diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts index 9852216e438..043d67dd2b9 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -1,13 +1,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Header', () => { - it('should pass accessibility tests', () => { - cy.visit('/'); + it('should pass accessibility tests', () => { + cy.visit('/'); - // Header must first be visible - cy.get('ds-header').should('be.visible'); + // Header must first be visible + cy.get('ds-header').should('be.visible'); - // Analyze for accessibility - testA11y('ds-header'); - }); + // Analyze for accessibility + testA11y('ds-header'); + }); }); diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts index ece38686b93..f9642c0c831 100644 --- a/cypress/e2e/homepage-statistics.cy.ts +++ b/cypress/e2e/homepage-statistics.cy.ts @@ -1,31 +1,32 @@ +import '../support/commands'; + import { REGEX_MATCH_NON_EMPTY_TEXT } 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[data-test="link-menu-item.menu.section.statistics"]').click(); - cy.location('pathname').should('eq', '/statistics'); - }); + it('should load if you click on "Statistics" from homepage', () => { + cy.visit('/'); + cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); + cy.location('pathname').should('eq', '/statistics'); + }); - it('should pass accessibility tests', () => { - // generate 2 view events on an Item's page - cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); - cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); + it('should pass accessibility tests', () => { + // generate 2 view events on an Item's page + cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); + cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); - cy.visit('/statistics'); + cy.visit('/statistics'); - // tag must be visable - cy.get('ds-site-statistics-page').should('be.visible'); + // 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); + // 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'); - }); + // Analyze for accessibility issues + testA11y('ds-site-statistics-page'); + }); }); diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts index b4c01a1a946..b13d5a46958 100644 --- a/cypress/e2e/item-edit.cy.ts +++ b/cypress/e2e/item-edit.cy.ts @@ -1,135 +1,135 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit'); beforeEach(() => { - // All tests start with visiting the Edit Item Page - cy.visit(ITEM_EDIT_PAGE); + // All tests start with visiting the Edit Item Page + cy.visit(ITEM_EDIT_PAGE); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); }); describe('Edit Item > Edit Metadata tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="metadata"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="metadata"]').click(); - // tag must be loaded - cy.get('ds-edit-item-page').should('be.visible'); + // tag must be loaded + cy.get('ds-edit-item-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-edit-item-page'); - }); + // Analyze for accessibility issues + testA11y('ds-edit-item-page'); + }); }); describe('Edit Item > Status tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="status"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="status"]').click(); - // tag must be loaded - cy.get('ds-item-status').should('be.visible'); + // tag must be loaded + cy.get('ds-item-status').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-status'); - }); + // Analyze for accessibility issues + testA11y('ds-item-status'); + }); }); describe('Edit Item > Bitstreams tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="bitstreams"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="bitstreams"]').click(); - // tag must be loaded - cy.get('ds-item-bitstreams').should('be.visible'); + // tag must be loaded + cy.get('ds-item-bitstreams').should('be.visible'); - // Table of item bitstreams must also be loaded - cy.get('div.item-bitstreams').should('be.visible'); + // Table of item bitstreams must also be loaded + cy.get('div.item-bitstreams').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-bitstreams', + // Analyze for accessibility issues + testA11y('ds-item-bitstreams', { - rules: { - // Currently Bitstreams page loads a pagination component per Bundle - // and they all use the same 'id="p-dad"'. - 'duplicate-id': { enabled: false }, - } - } as Options - ); - }); + rules: { + // Currently Bitstreams page loads a pagination component per Bundle + // and they all use the same 'id="p-dad"'. + 'duplicate-id': { enabled: false }, + }, + } as Options, + ); + }); }); describe('Edit Item > Curate tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="curate"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); - // tag must be loaded - cy.get('ds-item-curate').should('be.visible'); + // tag must be loaded + cy.get('ds-item-curate').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-curate'); - }); + // Analyze for accessibility issues + testA11y('ds-item-curate'); + }); }); describe('Edit Item > Relationships tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="relationships"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="relationships"]').click(); - // tag must be loaded - cy.get('ds-item-relationships').should('be.visible'); + // tag must be loaded + cy.get('ds-item-relationships').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-relationships'); - }); + // Analyze for accessibility issues + testA11y('ds-item-relationships'); + }); }); describe('Edit Item > Version History tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="versionhistory"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="versionhistory"]').click(); - // tag must be loaded - cy.get('ds-item-version-history').should('be.visible'); + // tag must be loaded + cy.get('ds-item-version-history').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-version-history'); - }); + // Analyze for accessibility issues + testA11y('ds-item-version-history'); + }); }); describe('Edit Item > Access Control tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="access-control"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').click(); - // tag must be loaded - cy.get('ds-item-access-control').should('be.visible'); + // tag must be loaded + cy.get('ds-item-access-control').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-access-control'); - }); + // Analyze for accessibility issues + testA11y('ds-item-access-control'); + }); }); describe('Edit Item > Collection Mapper tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="mapper"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="mapper"]').click(); - // tag must be loaded - cy.get('ds-item-collection-mapper').should('be.visible'); + // tag must be loaded + cy.get('ds-item-collection-mapper').should('be.visible'); - // Analyze entire page for accessibility issues - testA11y('ds-item-collection-mapper'); + // Analyze entire page for accessibility issues + testA11y('ds-item-collection-mapper'); - // Click on the "Map new collections" tab - cy.get('li[data-test="mapTab"] a').click(); + // Click on the "Map new collections" tab + cy.get('li[data-test="mapTab"] a').click(); - // Make sure search form is now visible - cy.get('ds-search-form').should('be.visible'); + // Make sure search form is now visible + cy.get('ds-search-form').should('be.visible'); - // Analyze entire page (again) for accessibility issues - testA11y('ds-item-collection-mapper'); - }); + // Analyze entire page (again) for accessibility issues + testA11y('ds-item-collection-mapper'); + }); }); diff --git a/cypress/e2e/item-page.cy.ts b/cypress/e2e/item-page.cy.ts index a6a208e9f45..b79b6ac31d1 100644 --- a/cypress/e2e/item-page.cy.ts +++ b/cypress/e2e/item-page.cy.ts @@ -1,32 +1,32 @@ import { testA11y } from 'cypress/support/utils'; describe('Item Page', () => { - const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); - const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_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); - }); + // 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); + it('should pass accessibility tests', () => { + cy.visit(ENTITYPAGE); - // tag must be loaded - cy.get('ds-item-page').should('be.visible'); + // tag must be loaded + cy.get('ds-item-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-page'); - }); + // Analyze for accessibility issues + testA11y('ds-item-page'); + }); - it('should pass accessibility tests on full item page', () => { - cy.visit(ENTITYPAGE + '/full'); + 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'); + // tag must be loaded + cy.get('ds-full-item-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-full-item-page'); - }); + // 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 index 6caeacae8e1..6518f595a90 100644 --- a/cypress/e2e/item-statistics.cy.ts +++ b/cypress/e2e/item-statistics.cy.ts @@ -2,42 +2,42 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Item Statistics Page', () => { - const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); - - it('should load if you click on "Statistics" from an Item/Entity page', () => { - cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); - cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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('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(Cypress.env('DSPACE_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); - - // Analyze for accessibility issues - testA11y('ds-item-statistics-page'); - }); + const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + + it('should load if you click on "Statistics" from an Item/Entity page', () => { + cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); + cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.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('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(Cypress.env('DSPACE_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); + + // 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 index 673041e9f39..190f3ff9271 100644 --- a/cypress/e2e/login-modal.cy.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -1,150 +1,150 @@ import { testA11y } from 'cypress/support/utils'; const page = { - openLoginMenu() { - // Click the "Log In" dropdown menu in header - cy.get('ds-themed-header [data-test="login-menu"]').click(); - }, - openUserMenu() { - // Once logged in, click the User menu in header - cy.get('ds-themed-header [data-test="user-menu"]').click(); - }, - submitLoginAndPasswordByPressingButton(email, password) { - // Enter email - cy.get('ds-themed-header [data-test="email"]').type(email); - // Enter password - cy.get('ds-themed-header [data-test="password"]').type(password); - // Click login button - cy.get('ds-themed-header [data-test="login-button"]').click(); - }, - submitLoginAndPasswordByPressingEnter(email, password) { - // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-header [data-test="email"]').type(email); - cy.get('ds-themed-header [data-test="password"]').type(password); - cy.get('ds-themed-header [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-header [data-test="logout-button"]').click(); - // Wait until above POST command responds before continuing - // (This ensures next action waits until logout completes) - cy.wait('@logout'); - } + openLoginMenu() { + // Click the "Log In" dropdown menu in header + cy.get('ds-themed-header [data-test="login-menu"]').click(); + }, + openUserMenu() { + // Once logged in, click the User menu in header + cy.get('ds-themed-header [data-test="user-menu"]').click(); + }, + submitLoginAndPasswordByPressingButton(email, password) { + // Enter email + cy.get('ds-themed-header [data-test="email"]').type(email); + // Enter password + cy.get('ds-themed-header [data-test="password"]').type(password); + // Click login button + cy.get('ds-themed-header [data-test="login-button"]').click(); + }, + submitLoginAndPasswordByPressingEnter(email, password) { + // In opened Login modal, fill out email & password, then click Enter + cy.get('ds-themed-header [data-test="email"]').type(email); + cy.get('ds-themed-header [data-test="password"]').type(password); + cy.get('ds-themed-header [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-header [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(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); - cy.visit(ENTITYPAGE); + it('should login when clicking button & stay on same page', () => { + const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + cy.visit(ENTITYPAGE); - // Login menu should exist - cy.get('ds-log-in').should('exist'); + // 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'); + // Login, and the tag should no longer exist + page.openLoginMenu(); + cy.get('.form-login').should('be.visible'); - page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.get('ds-log-in').should('not.exist'); + page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + cy.get('ds-log-in').should('not.exist'); - // Verify we are still on the same page - cy.url().should('include', ENTITYPAGE); + // 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'); - }); + // 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'); + 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'); + // 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(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.get('.form-login').should('not.exist'); + // Login, and the tag should no longer exist + page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + cy.get('.form-login').should('not.exist'); - // Verify we are still on homepage - cy.url().should('include', '/home'); + // 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'); - }); + // 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(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.visit('/'); + it('should support logout', () => { + // First authenticate & access homepage + cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_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'); + // 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(); + // 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'); - }); + // 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('/'); + it('should allow new user registration', () => { + cy.visit('/'); - page.openLoginMenu(); + page.openLoginMenu(); - // Registration link should be visible - cy.get('ds-themed-header [data-test="register"]').should('be.visible'); + // Registration link should be visible + cy.get('ds-themed-header [data-test="register"]').should('be.visible'); - // Click registration link & you should go to registration page - cy.get('ds-themed-header [data-test="register"]').click(); - cy.location('pathname').should('eq', '/register'); - cy.get('ds-register-email').should('exist'); + // Click registration link & you should go to registration page + cy.get('ds-themed-header [data-test="register"]').click(); + cy.location('pathname').should('eq', '/register'); + cy.get('ds-register-email').should('exist'); - // Test accessibility of this page - testA11y('ds-register-email'); - }); + // Test accessibility of this page + testA11y('ds-register-email'); + }); - it('should allow forgot password', () => { - cy.visit('/'); + it('should allow forgot password', () => { + cy.visit('/'); - page.openLoginMenu(); + page.openLoginMenu(); - // Forgot password link should be visible - cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); + // Forgot password link should be visible + cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); - // Click link & you should go to Forgot Password page - cy.get('ds-themed-header [data-test="forgot"]').click(); - cy.location('pathname').should('eq', '/forgot'); - cy.get('ds-forgot-email').should('exist'); + // Click link & you should go to Forgot Password page + cy.get('ds-themed-header [data-test="forgot"]').click(); + cy.location('pathname').should('eq', '/forgot'); + cy.get('ds-forgot-email').should('exist'); - // Test accessibility of this page - testA11y('ds-forgot-email'); - }); + // Test accessibility of this page + testA11y('ds-forgot-email'); + }); - it('should pass accessibility tests in menus', () => { - cy.visit('/'); + it('should pass accessibility tests in menus', () => { + cy.visit('/'); - // Open login menu & verify accessibility - page.openLoginMenu(); - cy.get('ds-log-in').should('exist'); - testA11y('ds-log-in'); + // Open login menu & verify accessibility + page.openLoginMenu(); + cy.get('ds-log-in').should('exist'); + testA11y('ds-log-in'); - // Now login - page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.get('ds-log-in').should('not.exist'); + // Now login + page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + cy.get('ds-log-in').should('not.exist'); - // Open user menu, verify user menu accesibility - page.openUserMenu(); - cy.get('ds-user-menu').should('be.visible'); - testA11y('ds-user-menu'); - }); + // Open user menu, verify user menu accesibility + page.openUserMenu(); + cy.get('ds-user-menu').should('be.visible'); + testA11y('ds-user-menu'); + }); }); diff --git a/cypress/e2e/my-dspace.cy.ts b/cypress/e2e/my-dspace.cy.ts index c48656ffcc0..159bb4f5e65 100644 --- a/cypress/e2e/my-dspace.cy.ts +++ b/cypress/e2e/my-dspace.cy.ts @@ -1,134 +1,134 @@ import { testA11y } from 'cypress/support/utils'; describe('My DSpace page', () => { - it('should display recent submissions and pass accessibility tests', () => { - cy.visit('/mydspace'); + 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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); - cy.get('ds-my-dspace-page').should('be.visible'); + 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'); + // 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 }); + // 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'); - }); + // Analyze for accessibility issues + testA11y('ds-my-dspace-page'); + }); - it('should have a working detailed view that passes accessibility tests', () => { - cy.visit('/mydspace'); + 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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); - cy.get('ds-my-dspace-page').should('be.visible'); + 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(); + // 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'); + cy.get('ds-object-detail').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-my-dspace-page'); - }); + // Analyze for accessibility issues + testA11y('ds-my-dspace-page'); + }); - // 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'); + // 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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_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(); + // 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'); + // 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(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + // Type in a known Collection name in the search box + cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); - // Click on the button matching that known Collection name - cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click(); + // Click on the button matching that known Collection name + cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click(); - // New URL should include /workspaceitems, as we've started a new submission - cy.url().should('include', '/workspaceitems'); + // 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'); + // 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', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + // A Collection menu button should exist & its value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_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]; + // 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(); + // 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'); + // "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}); + // 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(); + // 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'); + // 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(); + // 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'); + // 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(); + // 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'); - }); + // Discarding should send us back to MyDSpace + cy.url().should('include', '/mydspace'); }); + }); - it('should let you import from external sources', () => { - cy.visit('/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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_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(); + // 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'); + // 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'); + // The external import searchbox should be visible + cy.get('ds-submission-import-external-searchbar').should('be.visible'); - // Test for accessibility issues - testA11y('ds-submission-import-external'); - }); + // Test for accessibility issues + testA11y('ds-submission-import-external'); + }); }); diff --git a/cypress/e2e/pagenotfound.cy.ts b/cypress/e2e/pagenotfound.cy.ts index d02aa8541c3..968ae2747b5 100644 --- a/cypress/e2e/pagenotfound.cy.ts +++ b/cypress/e2e/pagenotfound.cy.ts @@ -1,18 +1,18 @@ 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('be.visible'); + 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('be.visible'); - // Analyze for accessibility issues - testA11y('ds-pagenotfound'); - }); + // Analyze for accessibility issues + testA11y('ds-pagenotfound'); + }); - it('should not contain element ds-pagenotfound when navigating to existing page', () => { - cy.visit('/home'); - cy.get('ds-pagenotfound').should('not.exist'); - }); + it('should not contain element ds-pagenotfound when navigating to existing page', () => { + cy.visit('/home'); + cy.get('ds-pagenotfound').should('not.exist'); + }); }); diff --git a/cypress/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts index 28a72bcc786..b1682199161 100644 --- a/cypress/e2e/search-navbar.cy.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,64 +1,64 @@ const page = { - fillOutQueryInNavBar(query) { - // Click the magnifying glass - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); - // Fill out a query in input that appears - cy.get('ds-themed-header [data-test="header-search-box"]').type(query); - }, - submitQueryByPressingEnter() { - cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); - }, - submitQueryByPressingIcon() { - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); - } + fillOutQueryInNavBar(query) { + // Click the magnifying glass + cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + // Fill out a query in input that appears + cy.get('ds-themed-header [data-test="header-search-box"]').type(query); + }, + submitQueryByPressingEnter() { + cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); + }, + submitQueryByPressingIcon() { + cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + }, }; describe('Search from Navigation Bar', () => { - // NOTE: these tests currently assume this query will return results! - const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); + // NOTE: these tests currently assume this query will return results! + const query = Cypress.env('DSPACE_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 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 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'); - }); + 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 index 429f4e6da46..62e73c38772 100644 --- a/cypress/e2e/search-page.cy.ts +++ b/cypress/e2e/search-page.cy.ts @@ -1,57 +1,57 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; describe('Search Page', () => { - // NOTE: these tests currently assume this query will return results! - const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); + // NOTE: these tests currently assume this query will return results! + const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); - 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 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(query)); - cy.get('[data-test="search-box"]').should('have.value', query); + it('should load results and pass accessibility tests', () => { + cy.visit('/search?query='.concat(query)); + cy.get('[data-test="search-box"]').should('have.value', query); - // tag must be loaded - cy.get('ds-search-page').should('be.visible'); + // 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'); + // 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 }); + // 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'); - }); + // Analyze for accessibility issues + testA11y('ds-search-page'); + }); - it('should have a working grid view that passes accessibility tests', () => { - cy.visit('/search?query='.concat(query)); + it('should have a working grid view that passes accessibility tests', () => { + cy.visit('/search?query='.concat(query)); - // Click button in sidebar to display grid view - cy.get('ds-search-sidebar [data-test="grid-view"]').click(); + // 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'); + // 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'); + // 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', + // Analyze for accessibility issues + testA11y('ds-search-page', { - rules: { - // Card titles fail this test currently - 'heading-order': { enabled: false } - } - } as Options - ); - }); + rules: { + // Card titles fail this test currently + 'heading-order': { enabled: false }, + }, + } as Options, + ); + }); }); diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts index 4402410f234..7123d841345 100644 --- a/cypress/e2e/submission.cy.ts +++ b/cypress/e2e/submission.cy.ts @@ -4,224 +4,224 @@ import { Options } from 'cypress-axe'; describe('New Submission page', () => { - // NOTE: We already test that new Item 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(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + // NOTE: We already test that new Item 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(Cypress.env('DSPACE_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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); - // Should redirect to /workspaceitems, as we've started a new submission - cy.url().should('include', '/workspaceitems'); + // 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'); + // 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', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + // A Collection menu button should exist & it's value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_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'); + // 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'); - // Test entire page for accessibility - testA11y('ds-submission-edit', + // Test entire page for accessibility + testA11y('ds-submission-edit', { - rules: { - // Author & Subject fields have invalid "aria-multiline" attrs. - // See https://github.com/DSpace/dspace-angular/issues/1272 - 'aria-allowed-attr': { enabled: false }, - // All panels are accordians & fail "aria-required-children" and "nested-interactive". - // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 - 'aria-required-children': { enabled: false }, - 'nested-interactive': { enabled: false }, - // All select boxes fail to have a name / aria-label. - // This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216 - 'select-name': { enabled: false }, - } - - } as Options - ); - - // 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(); + rules: { + // Author & Subject fields have invalid "aria-multiline" attrs. + // See https://github.com/DSpace/dspace-angular/issues/1272 + 'aria-allowed-attr': { enabled: false }, + // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + // All select boxes fail to have a name / aria-label. + // This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216 + 'select-name': { enabled: false }, + }, + + } as Options, + ); + + // 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(Cypress.env('DSPACE_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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_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 block submission & show errors if required fields are missing', () => { - // Create a new submission - cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + it('should allow for deposit if all required fields completed & file uploaded', () => { + // Create a new submission + cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_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(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); - // Attempt an immediate deposit without filling out any fields - cy.get('button#deposit').click(); + // 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'); - // 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'); + // 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 } ); - // 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'); + // 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'); - // Title field should have class "is-invalid" applied, as it's required - cy.get('input#dc_title').should('have.class', 'is-invalid'); + // This is the POST command that will upload the file + cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); - // 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(); - }); + // 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', }); - it('should allow for deposit if all required fields completed & file uploaded', () => { - // Create a new submission - cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + // Wait for upload to complete before proceeding + cy.wait('@upload'); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + // Wait for deposit button to not be disabled & click it. + cy.get('button#deposit').should('not.be.disabled').click(); - // 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'); + // 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'); + }); - // 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} ); + it('is possible to submit a new "Person" and that form passes accessibility', () => { + // To submit a different entity type, we'll start from MyDSpace + cy.visit('/mydspace'); - // 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 page is restricted, so we will be shown the login form. Fill it out & submit. + // NOTE: At this time, we MUST login as admin to submit Person objects + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - // This is the POST command that will upload the file - cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); + // Open the New Submission dropdown + cy.get('button[data-test="submission-dropdown"]').click(); + // Click on the "Person" type in that dropdown + cy.get('#entityControlsDropdownMenu button[title="Person"]').click(); - // 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' - }); + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); - // Wait for upload to complete before proceeding - cy.wait('@upload'); + // Type in a known Collection name in the search box + cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')); - // 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'); - }); + // Click on the button matching that known Collection name + cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click(); - it('is possible to submit a new "Person" and that form passes accessibility', () => { - // To submit a different entity type, we'll start from MyDSpace - cy.visit('/mydspace'); + // New URL should include /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - // NOTE: At this time, we MUST login as admin to submit Person objects - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); - // Open the New Submission dropdown - cy.get('button[data-test="submission-dropdown"]').click(); - // Click on the "Person" type in that dropdown - cy.get('#entityControlsDropdownMenu button[title="Person"]').click(); + // A Collection menu button should exist & its value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')); - // This should display the (popup window) - cy.get('ds-create-item-parent-selector').should('be.visible'); + // 3 sections should be visible by default + cy.get('div#section_personStep').should('be.visible'); + cy.get('div#section_upload').should('be.visible'); + cy.get('div#section_license').should('be.visible'); - // Type in a known Collection name in the search box - cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')); - - // Click on the button matching that known Collection name - cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_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', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')); - - // 3 sections should be visible by default - cy.get('div#section_personStep').should('be.visible'); - cy.get('div#section_upload').should('be.visible'); - cy.get('div#section_license').should('be.visible'); - - // Test entire page for accessibility - testA11y('ds-submission-edit', + // Test entire page for accessibility + testA11y('ds-submission-edit', { - rules: { - // All panels are accordians & fail "aria-required-children" and "nested-interactive". - // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 - 'aria-required-children': { enabled: false }, - 'nested-interactive': { enabled: false }, - } - - } as Options - ); - - // Click the lookup button next to "Publication" field - cy.get('button[data-test="lookup-button"]').click(); - - // A popup modal window should be visible - cy.get('ds-dynamic-lookup-relation-modal').should('be.visible'); - - // Popup modal should also pass accessibility tests - //testA11y('ds-dynamic-lookup-relation-modal'); - testA11y({ - include: ['ds-dynamic-lookup-relation-modal'], - exclude: [ - ['ul.nav-tabs'] // Tabs at top of model have several issues which seem to be caused by ng-bootstrap - ], - }); - - // Close popup window - cy.get('ds-dynamic-lookup-relation-modal button.close').click(); - - // Back on the form, click the discard button to remove new submission - // Clicking it will display a confirmation, which we will confirm with another click - cy.get('button#discard').click(); - cy.get('button#discard_submit').click(); + rules: { + // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + }, + + } as Options, + ); + + // Click the lookup button next to "Publication" field + cy.get('button[data-test="lookup-button"]').click(); + + // A popup modal window should be visible + cy.get('ds-dynamic-lookup-relation-modal').should('be.visible'); + + // Popup modal should also pass accessibility tests + //testA11y('ds-dynamic-lookup-relation-modal'); + testA11y({ + include: ['ds-dynamic-lookup-relation-modal'], + exclude: [ + ['ul.nav-tabs'], // Tabs at top of model have several issues which seem to be caused by ng-bootstrap + ], }); + + // Close popup window + cy.get('ds-dynamic-lookup-relation-modal button.close').click(); + + // Back on the form, click the discard button to remove new submission + // Clicking it will display a confirmation, which we will confirm with another click + cy.get('button#discard').click(); + cy.get('button#discard_submit').click(); + }); }); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index cc3dccba38e..091f11d0f7e 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -9,51 +9,51 @@ let REST_DOMAIN: string; // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // For more info, visit https://on.cypress.io/plugins-api module.exports = (on, config) => { - on('task', { - // Define "log" and "table" tasks, used for logging accessibility errors during CI - // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file - log(message: string) { - console.log(message); - return null; - }, - table(message: string) { - console.table(message); - return null; - }, - // 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. - readUIConfig() { - // Check if we have a config.json in the src/assets. If so, use that. - // This is where it's written when running "ng e2e" or "yarn serve" - if (fs.existsSync('./src/assets/config.json')) { - return fs.readFileSync('./src/assets/config.json', 'utf8'); - // Otherwise, check the dist/browser/assets - // This is where it's written when running "serve:ssr", which is what CI uses to start the frontend - } else if (fs.existsSync('./dist/browser/assets/config.json')) { - return fs.readFileSync('./dist/browser/assets/config.json', 'utf8'); - } + on('task', { + // Define "log" and "table" tasks, used for logging accessibility errors during CI + // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file + log(message: string) { + console.log(message); + return null; + }, + table(message: string) { + console.table(message); + return null; + }, + // 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. + readUIConfig() { + // Check if we have a config.json in the src/assets. If so, use that. + // This is where it's written when running "ng e2e" or "yarn serve" + if (fs.existsSync('./src/assets/config.json')) { + return fs.readFileSync('./src/assets/config.json', 'utf8'); + // Otherwise, check the dist/browser/assets + // This is where it's written when running "serve:ssr", which is what CI uses to start the frontend + } else if (fs.existsSync('./dist/browser/assets/config.json')) { + return fs.readFileSync('./dist/browser/assets/config.json', 'utf8'); + } - return null; - }, - // Save value of REST Base URL, looked up before all tests. - // This allows other tests to use it easily via getRestBaseURL() below. - saveRestBaseURL(url: string) { - return (REST_BASE_URL = url); - }, - // Retrieve currently saved value of REST Base URL - getRestBaseURL() { - return REST_BASE_URL ; - }, - // Save value of REST Domain, looked up before all tests. - // This allows other tests to use it easily via getRestBaseDomain() below. - saveRestBaseDomain(domain: string) { - return (REST_DOMAIN = domain); - }, - // Retrieve currently saved value of REST Domain - getRestBaseDomain() { - return REST_DOMAIN ; - } - }); + return null; + }, + // Save value of REST Base URL, looked up before all tests. + // This allows other tests to use it easily via getRestBaseURL() below. + saveRestBaseURL(url: string) { + return (REST_BASE_URL = url); + }, + // Retrieve currently saved value of REST Base URL + getRestBaseURL() { + return REST_BASE_URL ; + }, + // Save value of REST Domain, looked up before all tests. + // This allows other tests to use it easily via getRestBaseDomain() below. + saveRestBaseDomain(domain: string) { + return (REST_DOMAIN = domain); + }, + // Retrieve currently saved value of REST Domain + getRestBaseDomain() { + return REST_DOMAIN ; + }, + }); }; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7da454e2d0c..b3e3b9630bb 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -3,8 +3,14 @@ // See docs at https://docs.cypress.io/api/cypress-api/custom-commands // *********************************************** -import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; -import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants'; +import { + AuthTokenInfo, + TOKENITEM, +} from 'src/app/core/auth/models/auth-token-info.model'; +import { + DSPACE_XSRF_COOKIE, + XSRF_REQUEST_HEADER, +} from 'src/app/core/xsrf/xsrf.constants'; import { v4 as uuidv4 } from 'uuid'; // Declare Cypress namespace to help with Intellisense & code completion in IDEs @@ -57,33 +63,33 @@ declare global { * @param password password to login as */ function login(email: string, password: string): void { - // Create a fake CSRF cookie/token to use in POST - cy.createCSRFCookie().then((csrfToken: string) => { - // get our REST API's base URL, also needed for POST - cy.task('getRestBaseURL').then((baseRestUrl: string) => { - // 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'); + // Create a fake CSRF cookie/token to use in POST + cy.createCSRFCookie().then((csrfToken: string) => { + // get our REST API's base URL, also needed for POST + cy.task('getRestBaseURL').then((baseRestUrl: string) => { + // 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); + // 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)); - }); - }); + // 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)); + }); }); + }); } // Add as a Cypress command (i.e. assign to 'cy.login') Cypress.Commands.add('login', login); @@ -94,12 +100,12 @@ Cypress.Commands.add('login', login); * @param password password to login as */ function loginViaForm(email: string, password: string): void { - // Enter email - cy.get('ds-log-in [data-test="email"]').type(email); - // Enter password - cy.get('ds-log-in [data-test="password"]').type(password); - // Click login button - cy.get('ds-log-in [data-test="login-button"]').click(); + // Enter email + cy.get('ds-log-in [data-test="email"]').type(email); + // Enter password + cy.get('ds-log-in [data-test="password"]').type(password); + // Click login button + cy.get('ds-log-in [data-test="login-button"]').click(); } // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); @@ -117,29 +123,29 @@ Cypress.Commands.add('loginViaForm', loginViaForm); * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") */ function generateViewEvent(uuid: string, dsoType: string): void { - // Create a fake CSRF cookie/token to use in POST - cy.createCSRFCookie().then((csrfToken: string) => { - // get our REST API's base URL, also needed for POST - cy.task('getRestBaseURL').then((baseRestUrl: string) => { - // 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); - }); - }); + // Create a fake CSRF cookie/token to use in POST + cy.createCSRFCookie().then((csrfToken: string) => { + // get our REST API's base URL, also needed for POST + cy.task('getRestBaseURL').then((baseRestUrl: string) => { + // 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); + }); }); + }); } // Add as a Cypress command (i.e. assign to 'cy.generateViewEvent') Cypress.Commands.add('generateViewEvent', generateViewEvent); @@ -153,17 +159,17 @@ Cypress.Commands.add('generateViewEvent', generateViewEvent); * @returns a Cypress Chainable which can be used to get the generated CSRF Token */ function createCSRFCookie(): Cypress.Chainable { - // Generate a new token which is a random UUID - const csrfToken: string = uuidv4(); + // Generate a new token which is a random UUID + const csrfToken: string = uuidv4(); - // Save it to our required cookie - cy.task('getRestBaseDomain').then((baseDomain: string) => { - // Create a fake CSRF Token. Set it in the required server-side cookie - cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); - }); + // Save it to our required cookie + cy.task('getRestBaseDomain').then((baseDomain: string) => { + // Create a fake CSRF Token. Set it in the required server-side cookie + cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); + }); - // return the generated token wrapped in a chainable - return cy.wrap(csrfToken); + // return the generated token wrapped in a chainable + return cy.wrap(csrfToken); } // Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie') Cypress.Commands.add('createCSRFCookie', createCSRFCookie); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f6c68650528..73d3c76a990 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -15,12 +15,11 @@ // 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'; -import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; +import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; // Runs once before all tests before(() => { @@ -35,18 +34,18 @@ before(() => { // Find URL of our REST API & save to global variable via task 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); + console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); } else { - baseRestUrl = config.rest.baseUrl; + baseRestUrl = config.rest.baseUrl; } cy.task('saveRestBaseURL', baseRestUrl); // Find domain of our REST API & save to global variable via task. 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); + console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); } else { - baseDomain = config.rest.host; + baseDomain = config.rest.host; } cy.task('saveRestBaseDomain', baseDomain); @@ -55,12 +54,12 @@ before(() => { // 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}'); + // 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}'); - // Remove any CSRF cookies saved from prior tests - cy.clearCookie(DSPACE_XSRF_COOKIE); + // Remove any CSRF cookies saved from prior tests + cy.clearCookie(DSPACE_XSRF_COOKIE); }); // NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL diff --git a/cypress/support/utils.ts b/cypress/support/utils.ts index 96575969e85..9a9ea1121ba 100644 --- a/cypress/support/utils.ts +++ b/cypress/support/utils.ts @@ -5,26 +5,26 @@ import { Options } from 'cypress-axe'; // Uses 'log' and 'table' tasks defined in ../plugins/index.ts // Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file function terminalLog(violations: Result[]) { - cy.task( - 'log', - `${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected` - ); - // pluck specific keys to keep the table readable - const violationData = violations.map( - ({ id, impact, description, helpUrl, nodes }) => ({ - id, - impact, - description, - helpUrl, - nodes: nodes.length, - html: nodes.map(node => node.html) - }) - ); + cy.task( + 'log', + `${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`, + ); + // pluck specific keys to keep the table readable + const violationData = violations.map( + ({ id, impact, description, helpUrl, nodes }) => ({ + id, + impact, + description, + helpUrl, + nodes: nodes.length, + html: nodes.map(node => node.html), + }), + ); - // Print violations as an array, since 'node.html' above often breaks table alignment - cy.task('log', violationData); - // Optionally, uncomment to print as a table - // cy.task('table', violationData); + // Print violations as an array, since 'node.html' above often breaks table alignment + cy.task('log', violationData); + // Optionally, uncomment to print as a table + // cy.task('table', violationData); } @@ -32,13 +32,13 @@ function terminalLog(violations: Result[]) { // while also ensuring any violations are logged to the terminal (see terminalLog above) // This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load export const testA11y = (context?: any, options?: Options) => { - cy.injectAxe(); - cy.configureAxe({ - rules: [ - // Disable color contrast checks as they are inaccurate / result in a lot of false positives - // See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast - { id: 'color-contrast', enabled: false }, - ] - }); - cy.checkA11y(context, options, terminalLog); + cy.injectAxe(); + cy.configureAxe({ + rules: [ + // Disable color contrast checks as they are inaccurate / result in a lot of false positives + // See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast + { id: 'color-contrast', enabled: false }, + ], + }); + cy.checkA11y(context, options, terminalLog); }; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 58083003cda..51237b5e954 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -4,10 +4,11 @@ "**/*.ts" ], "compilerOptions": { + "sourceMap": false, "types": [ "cypress", "cypress-axe", "node" ] } -} \ No newline at end of file +} diff --git a/karma.conf.js b/karma.conf.js index 8418312b1ab..f96558bfaff 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,10 @@ module.exports = function (config) { ], client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser - captureConsole: false + captureConsole: false, + jasmine: { + failSpecWithNoExpectations: true + } }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/dspace-angular'), diff --git a/package.json b/package.json index abd25f41482..0571d166bc5 100644 --- a/package.json +++ b/package.json @@ -55,28 +55,28 @@ "ts-node": "10.2.1" }, "dependencies": { - "@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", + "@angular/animations": "^17.3.4", + "@angular/cdk": "^17.3.4", + "@angular/common": "^17.3.4", + "@angular/compiler": "^17.3.4", + "@angular/core": "^17.3.4", + "@angular/forms": "^17.3.4", + "@angular/localize": "17.3.4", + "@angular/platform-browser": "^17.3.4", + "@angular/platform-browser-dynamic": "^17.3.4", + "@angular/platform-server": "^17.3.4", + "@angular/router": "^17.3.4", + "@angular/ssr": "^17.3.0", "@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", + "@ng-dynamic-forms/core": "^16.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", + "@ngrx/effects": "^17.1.1", + "@ngrx/router-store": "^17.1.1", + "@ngrx/store": "^17.1.1", "@ngx-translate/core": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@types/grecaptcha": "^3.0.4", @@ -94,7 +94,7 @@ "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.9", - "express": "^4.18.2", + "express": "^4.19.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", @@ -110,17 +110,15 @@ "lodash": "^4.17.21", "lru-cache": "^7.14.1", "markdown-it": "^13.0.1", - "markdown-it-mathjax3": "^4.3.2", "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.11.0", "morgan": "^1.10.0", "ng-mocks": "^14.10.0", - "ng2-file-upload": "1.4.0", + "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", - "ngx-infinite-scroll": "^15.0.0", + "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", - "ngx-sortablejs": "^11.1.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", "pem": "1.14.7", @@ -128,28 +126,27 @@ "react-copy-to-clipboard": "^5.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "sanitize-html": "^2.10.0", + "sanitize-html": "^2.12.1", "sortablejs": "1.15.0", "uuid": "^8.3.2", "webfontloader": "1.6.28", - "zone.js": "~0.11.5" + "zone.js": "~0.14.4" }, "devDependencies": { - "@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": "^16.0.4", - "@angular/compiler-cli": "^15.2.8", - "@angular/language-service": "^15.2.8", + "@angular-builders/custom-webpack": "~17.0.1", + "@angular-devkit/build-angular": "^17.3.0", + "@angular-eslint/builder": "17.2.1", + "@angular-eslint/eslint-plugin": "17.2.1", + "@angular-eslint/eslint-plugin-template": "17.2.1", + "@angular-eslint/schematics": "17.2.1", + "@angular-eslint/template-parser": "17.2.1", + "@angular/cli": "^17.3.0", + "@angular/compiler-cli": "^17.3.4", + "@angular/language-service": "^17.3.4", "@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", + "@ngrx/store-devtools": "^17.1.1", + "@ngtools/webpack": "^16.2.12", "@types/deep-freeze": "0.1.2", "@types/ejs": "^3.1.2", "@types/express": "^4.17.17", @@ -161,6 +158,7 @@ "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", "axe-core": "^4.7.2", + "browser-sync": "^3.0.0", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", @@ -170,9 +168,12 @@ "eslint": "^8.39.0", "eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-import": "^2.27.5", + "eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-rxjs": "^5.0.3", + "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.1.7", "jasmine-core": "^3.8.0", @@ -183,7 +184,7 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ngx-mask": "^13.1.7", + "ngx-mask": "14.2.4", "nodemon": "^2.0.22", "postcss": "^8.4", "postcss-apply": "0.12.0", @@ -199,10 +200,10 @@ "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "typescript": "~4.8.4", + "typescript": "~5.3.3", "webpack": "5.76.1", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^4.2.0", "webpack-dev-server": "^4.13.3" } -} +} \ No newline at end of file diff --git a/server.ts b/server.ts index da085f372fd..22f34232874 100644 --- a/server.ts +++ b/server.ts @@ -17,7 +17,6 @@ import 'zone.js/node'; import 'reflect-metadata'; -import 'rxjs'; /* eslint-disable import/no-namespace */ import * as morgan from 'morgan'; @@ -39,23 +38,26 @@ import { join } from 'path'; import { enableProdMode } from '@angular/core'; -import { ngExpressEngine } from '@nguniversal/express-engine'; -import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasNoValue, hasValue } from './src/app/shared/empty.util'; - +import { hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; - -import { ServerAppModule } from './src/main.server'; - +import bootstrap from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; -import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; +import { + APP_CONFIG, + AppConfig, +} from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; - +import { CommonEngine } from '@angular/ssr'; +import { APP_BASE_HREF } from '@angular/common'; +import { + REQUEST, + RESPONSE, +} from './src/express.tokens'; /* * Set path for the browser application's dist folder @@ -127,27 +129,6 @@ export function app() { */ server.use(json()); - // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) - server.engine('html', (_, options, callback) => - ngExpressEngine({ - bootstrap: ServerAppModule, - providers: [ - { - provide: REQUEST, - useValue: (options as any).req, - }, - { - provide: RESPONSE, - useValue: (options as any).req.res, - }, - { - provide: APP_CONFIG, - useValue: environment - } - ] - })(_, (options as any), callback) - ); - server.engine('ejs', ejs.renderFile); /* @@ -162,7 +143,7 @@ export function app() { server.get('/robots.txt', (req, res) => { res.setHeader('content-type', 'text/plain'); res.render('assets/robots.txt.ejs', { - 'origin': req.protocol + '://' + req.headers.host + 'origin': req.protocol + '://' + req.headers.host, }); }); @@ -177,7 +158,7 @@ export function app() { router.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -186,7 +167,7 @@ export function app() { router.use('/signposting**', createProxyMiddleware({ target: `${environment.rest.baseUrl}`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -197,7 +178,7 @@ export function app() { const RateLimit = require('express-rate-limit'); const limiter = new RateLimit({ windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, - max: (environment.ui as UIServerConfig).rateLimiter.max + max: (environment.ui as UIServerConfig).rateLimiter.max, }); server.use(limiter); } @@ -236,10 +217,10 @@ export function app() { /* * The callback function to serve server side angular */ -function ngApp(req, res) { - if (environment.universal.preboot) { +function ngApp(req, res, next) { + if (environment.ssr.enabled) { // Render the page to user via SSR (server side rendering) - serverSideRender(req, res); + serverSideRender(req, res, next); } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct client-side rendering (CSR)'); @@ -252,45 +233,66 @@ function ngApp(req, res) { * returned to the user. * @param req current request * @param res current response + * @param next the next function * @param sendToUser if true (default), send the rendered content to the user. * If false, then only save this rendered content to the in-memory cache (to refresh cache). */ -function serverSideRender(req, res, sendToUser: boolean = true) { +function serverSideRender(req, res, next, sendToUser: boolean = true) { + const { protocol, originalUrl, baseUrl, headers } = req; + const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler }); // Render the page via SSR (server side rendering) - res.render(indexHtml, { - req, - res, - preboot: environment.universal.preboot, - async: environment.universal.async, - time: environment.universal.time, - baseUrl: environment.ui.nameSpace, - originUrl: environment.ui.baseUrl, - requestUrl: req.originalUrl, - }, (err, data) => { - if (hasNoValue(err) && hasValue(data)) { - // save server side rendered page to cache (if any are enabled) - saveToCache(req, data); - if (sendToUser) { - res.locals.ssr = true; // mark response as SSR (enables text compression) - // send rendered page to user - res.send(data); - } - } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { - // When this error occurs we can't fall back to CSR because the response has already been - // sent. These errors occur for various reasons in universal, not all of which are in our - // control to solve. - console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); - } else { - console.warn('Error in server-side rendering (SSR)'); - if (hasValue(err)) { - console.warn('Error details : ', err); + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + inlineCriticalCss: environment.ssr.inlineCriticalCss, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: DIST_FOLDER, + providers: [ + { provide: APP_BASE_HREF, useValue: baseUrl }, + { + provide: REQUEST, + useValue: req, + }, + { + provide: RESPONSE, + useValue: res, + }, + { + provide: APP_CONFIG, + useValue: environment, + }, + ], + }) + .then((html) => { + if (hasValue(html)) { + // save server side rendered page to cache (if any are enabled) + saveToCache(req, html); + if (sendToUser) { + res.locals.ssr = true; // mark response as SSR (enables text compression) + // send rendered page to user + res.send(html); + } } - if (sendToUser) { - console.warn('Falling back to serving direct client-side rendering (CSR).'); - clientSideRender(req, res); + }) + .catch((err) => { + if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { + // When this error occurs we can't fall back to CSR because the response has already been + // sent. These errors occur for various reasons in universal, not all of which are in our + // control to solve. + console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); + } else { + console.warn('Error in server-side rendering (SSR)'); + if (hasValue(err)) { + console.warn('Error details : ', err); + } + if (sendToUser) { + console.warn('Falling back to serving direct client-side rendering (CSR).'); + clientSideRender(req, res); + } } - } - }); + next(err); + }); } /** @@ -325,7 +327,7 @@ function initCache() { botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive, - allowStale: environment.cache.serverSide.botCache.allowStale + allowStale: environment.cache.serverSide.botCache.allowStale, }); } @@ -337,7 +339,7 @@ function initCache() { anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive, - allowStale: environment.cache.serverSide.anonymousCache.allowStale + allowStale: environment.cache.serverSide.anonymousCache.allowStale, }); } } @@ -348,7 +350,7 @@ function initCache() { function botCacheEnabled(): boolean { // Caching is only enabled if SSR is enabled AND // "max" pages to cache is greater than zero - return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); + return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); } /** @@ -357,7 +359,7 @@ function botCacheEnabled(): boolean { function anonymousCacheEnabled(): boolean { // Caching is only enabled if SSR is enabled AND // "max" pages to cache is greater than zero - return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); + return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); } /** @@ -370,9 +372,9 @@ function cacheCheck(req, res, next) { // If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page. if (botCacheEnabled() && isbot(req.get('user-agent'))) { - cachedCopy = checkCacheForRequest('bot', botCache, req, res); + cachedCopy = checkCacheForRequest('bot', botCache, req, res, next); } else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) { - cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res); + cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next); } // If cached copy exists, return it to the user. @@ -408,14 +410,15 @@ function cacheCheck(req, res, next) { * @param cache LRU cache to check * @param req current request to look for in the cache * @param res current response + * @param next the next function * @returns cached copy (if found) or undefined (if not found) */ -function checkCacheForRequest(cacheName: string, cache: LRU, req, res): any { +function checkCacheForRequest(cacheName: string, cache: LRU, req, res, next): any { // Get the cache key for this request const key = getCacheKey(req); // Check if this page is in our cache - let cachedCopy = cache.get(key); + const cachedCopy = cache.get(key); if (cachedCopy) { if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } @@ -426,7 +429,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU, req, r // Update cached copy by rerendering server-side // NOTE: In this scenario the currently cached copy will be returned to the current user. // This re-render is peformed behind the scenes to update cached copy for next user. - serverSideRender(req, res, false); + serverSideRender(req, res, next, false); } } else { if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } @@ -529,20 +532,20 @@ function serverStarted() { function createHttpsServer(keys) { const listener = createServer({ key: keys.serviceKey, - cert: keys.certificate - }, app).listen(environment.ui.port, environment.ui.host, () => { + cert: keys.certificate, + }, app()).listen(environment.ui.port, environment.ui.host, () => { serverStarted(); }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + 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'); - })(); - }); + void (async ()=> { + console.debug('Closing HTTPS server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTPS server closed'); + })(); + }); } /** @@ -559,14 +562,14 @@ function run() { }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + 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; - })(); - }); + 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() { @@ -597,7 +600,7 @@ function start() { if (serviceKey && certificate) { createHttpsServer({ serviceKey: serviceKey, - certificate: certificate + certificate: certificate, }); } else { console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); @@ -606,7 +609,7 @@ function start() { createCertificate({ days: 1, - selfSigned: true + selfSigned: true, }, (error, keys) => { createHttpsServer(keys); }); @@ -627,7 +630,7 @@ function healthCheck(req, res) { }) .catch((error) => { res.status(error.response.status).send({ - error: error.message + error: error.message, }); }); } diff --git a/src/app/access-control/access-control-routes.ts b/src/app/access-control/access-control-routes.ts new file mode 100644 index 00000000000..a7cce461ef0 --- /dev/null +++ b/src/app/access-control/access-control-routes.ts @@ -0,0 +1,117 @@ +import { AbstractControl } from '@angular/forms'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; +import { + DYNAMIC_ERROR_MESSAGES_MATCHER, + DynamicErrorMessagesMatcher, +} from '@ng-dynamic-forms/core'; + +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +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 { + EPERSON_PATH, + GROUP_PATH, +} from './access-control-routing-paths'; +import { BulkAccessComponent } from './bulk-access/bulk-access.component'; +import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; +import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; +import { EPersonResolver } from './epeople-registry/eperson-resolver.service'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { GroupPageGuard } from './group-registry/group-page.guard'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; + +/** + * Condition for displaying error messages on email form field + */ +export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = + (control: AbstractControl, model: any, hasFocus: boolean) => { + return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); + }; + +const providers = [ + { + provide: DYNAMIC_ERROR_MESSAGES_MATCHER, + useValue: ValidateEmailErrorStateMatcher, + }, +]; +export const ROUTES: Route[] = [ + { + path: EPERSON_PATH, + component: EPeopleRegistryComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, + canActivate: mapToCanActivate([SiteAdministratorGuard]), + }, + { + path: `${EPERSON_PATH}/create`, + component: EPersonFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, + canActivate: mapToCanActivate([SiteAdministratorGuard]), + }, + { + path: `${EPERSON_PATH}/:id/edit`, + component: EPersonFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + ePerson: EPersonResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, + canActivate: mapToCanActivate([SiteAdministratorGuard]), + }, + { + path: GROUP_PATH, + component: GroupsRegistryComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, + canActivate: mapToCanActivate([GroupAdministratorGuard]), + }, + { + path: `${GROUP_PATH}/create`, + component: GroupFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { + title: 'admin.access-control.groups.title.addGroup', + breadcrumbKey: 'admin.access-control.groups.addGroup', + }, + canActivate: mapToCanActivate([GroupAdministratorGuard]), + }, + { + path: `${GROUP_PATH}/:groupId/edit`, + component: GroupFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { + title: 'admin.access-control.groups.title.singleGroup', + breadcrumbKey: 'admin.access-control.groups.singleGroup', + }, + canActivate: mapToCanActivate([GroupPageGuard]), + }, + { + path: 'bulk-access', + component: BulkAccessComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, + canActivate: mapToCanActivate([SiteAdministratorGuard]), + }, +]; diff --git a/src/app/access-control/access-control-routing-paths.ts b/src/app/access-control/access-control-routing-paths.ts index 31f39f1c47d..06ae0321945 100644 --- a/src/app/access-control/access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -1,5 +1,5 @@ -import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAccessControlModuleRoute } from '../app-routing-paths'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; export const EPERSON_PATH = 'epeople'; diff --git a/src/app/access-control/access-control-routing.module.ts b/src/app/access-control/access-control-routing.module.ts deleted file mode 100644 index 97d049ad836..00000000000 --- a/src/app/access-control/access-control-routing.module.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { NgModule } from '@angular/core'; -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 { 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 { 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: EPERSON_PATH, - component: EPeopleRegistryComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, - canActivate: [SiteAdministratorGuard] - }, - { - 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 - }, - data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, - canActivate: [GroupAdministratorGuard] - }, - { - path: `${GROUP_PATH}/create`, - component: GroupFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }, - canActivate: [GroupAdministratorGuard] - }, - { - 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] - }, - ]) - ] -}) -/** - * Routing module for the AccessControl section of the admin sidebar - */ -export class AccessControlRoutingModule { - -} diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts deleted file mode 100644 index 3dc4b6cedc7..00000000000 --- a/src/app/access-control/access-control.module.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { SharedModule } from '../shared/shared.module'; -import { AccessControlRoutingModule } from './access-control-routing.module'; -import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; -import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; -import { GroupFormComponent } from './group-registry/group-form/group-form.component'; -import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; -import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; -import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; -import { FormModule } from '../shared/form/form.module'; -import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core'; -import { AbstractControl } from '@angular/forms'; -import { BulkAccessComponent } from './bulk-access/bulk-access.component'; -import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; -import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component'; -import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component'; -import { SearchModule } from '../shared/search/search.module'; -import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module'; - -/** - * Condition for displaying error messages on email form field - */ -export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = - (control: AbstractControl, model: any, hasFocus: boolean) => { - return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); - }; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - AccessControlRoutingModule, - FormModule, - NgbAccordionModule, - SearchModule, - AccessControlFormModule, - ], - exports: [ - MembersListComponent, - ], - declarations: [ - EPeopleRegistryComponent, - EPersonFormComponent, - GroupsRegistryComponent, - GroupFormComponent, - SubgroupsListComponent, - MembersListComponent, - BulkAccessComponent, - BulkAccessBrowseComponent, - BulkAccessSettingsComponent, - ], - providers: [ - { - provide: DYNAMIC_ERROR_MESSAGES_MATCHER, - useValue: ValidateEmailErrorStateMatcher - }, - ] -}) -/** - * This module handles all components related to the access control pages - */ -export class AccessControlModule { - -} 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 index 87b2a8d5684..f9eb487d73a 100644 --- 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 @@ -1,16 +1,28 @@ -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 { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + NgbAccordionModule, + NgbNavModule, +} from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; -import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; +import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; +import { SelectableListItemControlComponent } from '../../../shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.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 { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ThemedSearchComponent } from '../../../shared/search/themed-search.component'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; +import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; describe('BulkAccessBrowseComponent', () => { let component: BulkAccessBrowseComponent; @@ -23,7 +35,7 @@ describe('BulkAccessBrowseComponent', () => { const selected1 = new SelectableObject(value1); const selected2 = new SelectableObject(value2); - const testSelection = { id: listID1, selection: [selected1, selected2] } ; + const testSelection = { id: listID1, selection: [selected1, selected2] }; const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); beforeEach(waitForAsync(() => { @@ -31,14 +43,28 @@ describe('BulkAccessBrowseComponent', () => { imports: [ NgbAccordionModule, NgbNavModule, - TranslateModule.forRoot() + TranslateModule.forRoot(), + BulkAccessBrowseComponent, + ], + providers: [ + { provide: SelectableListService, useValue: selectableListService }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - declarations: [BulkAccessBrowseComponent], - providers: [ { provide: SelectableListService, useValue: selectableListService }, ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); + NO_ERRORS_SCHEMA, + ], + }) + .overrideComponent(BulkAccessBrowseComponent, { + remove: { + imports: [ + PaginationComponent, + ThemedSearchComponent, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -72,8 +98,8 @@ describe('BulkAccessBrowseComponent', () => { 'elementsPerPage': 5, 'totalElements': 2, 'totalPages': 1, - 'currentPage': 1 - }), [selected1, selected2]) ; + '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 index e806e729c8e..a400742f017 100644 --- 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 @@ -1,19 +1,48 @@ -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 { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + NgbAccordionModule, + NgbNavModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgxPaginationModule } from 'ngx-pagination'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; + +import { + buildPaginatedList, + PaginatedList, +} from '../../../core/data/paginated-list.model'; 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 { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service'; import { hasValue } from '../../../shared/empty.util'; +import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; +import { SelectableListItemControlComponent } from '../../../shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; +import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; +import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ThemedSearchComponent } from '../../../shared/search/themed-search.component'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; @Component({ selector: 'ds-bulk-access-browse', @@ -22,9 +51,24 @@ import { hasValue } from '../../../shared/empty.util'; providers: [ { provide: SEARCH_CONFIG_SERVICE, - useClass: SearchConfigurationService - } - ] + useClass: SearchConfigurationService, + }, + ], + imports: [ + PaginationComponent, + AsyncPipe, + NgbAccordionModule, + TranslateModule, + NgIf, + NgbNavModule, + ThemedSearchComponent, + BrowserOnlyPipe, + NgForOf, + NgxPaginationModule, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + standalone: true, }) export class BulkAccessBrowseComponent implements OnInit, OnDestroy { @@ -49,7 +93,7 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { paginationOptions$: BehaviorSubject = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { id: 'bas', pageSize: 5, - currentPage: 1 + currentPage: 1, })); /** @@ -67,20 +111,20 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + 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 + currentPage: this.paginationOptions$.value.currentPage + 1, })); } pagePrev() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: this.paginationOptions$.value.currentPage - 1 + currentPage: this.paginationOptions$.value.currentPage - 1, })); } @@ -99,12 +143,12 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { elementsPerPage: this.paginationOptions$.value.pageSize, totalElements: list?.selection.length, totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length), - currentPage: this.paginationOptions$.value.currentPage + 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 + currentPage: pageInfo.currentPage, })); } return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || [])); 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 index e9b253147dc..8bfbe1fe5d1 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.spec.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -1,18 +1,23 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { BulkAccessComponent } from './bulk-access.component'; +import { Process } from '../../process-page/processes/process.model'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; -import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; 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'; +import { ThemeService } from '../../shared/theme-support/theme.service'; +import { BulkAccessComponent } from './bulk-access.component'; +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; describe('BulkAccessComponent', () => { let component: BulkAccessComponent; @@ -31,35 +36,35 @@ describe('BulkAccessComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockFile = { 'uuids': [ - '1234', '5678' + '1234', '5678', ], - 'file': { } + 'file': { }, }; const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getValue: jasmine.createSpy('getValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; const selectableListState: SelectableListState = { id: 'test', selection }; @@ -71,16 +76,24 @@ describe('BulkAccessComponent', () => { await TestBed.configureTestingModule({ imports: [ RouterTestingModule, - TranslateModule.forRoot() + TranslateModule.forRoot(), + BulkAccessComponent, ], - declarations: [ BulkAccessComponent ], providers: [ { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: NotificationsService, useValue: NotificationsServiceStub }, - { provide: SelectableListService, useValue: selectableListServiceMock } + { provide: SelectableListService, useValue: selectableListServiceMock }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) + .overrideComponent(BulkAccessComponent, { + remove: { + imports: [ + BulkAccessSettingsComponent, + ], + }, + }) .compileComponents(); }); diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts index 04724614cb6..bd8e893b599 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -1,17 +1,34 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { + Component, + OnInit, + ViewChild, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; -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'; +import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; @Component({ selector: 'ds-bulk-access', templateUrl: './bulk-access.component.html', - styleUrls: ['./bulk-access.component.scss'] + styleUrls: ['./bulk-access.component.scss'], + imports: [ + TranslateModule, + BulkAccessSettingsComponent, + BulkAccessBrowseComponent, + ], + standalone: true, }) export class BulkAccessComponent implements OnInit { @@ -37,7 +54,7 @@ export class BulkAccessComponent implements OnInit { constructor( private bulkAccessControlService: BulkAccessControlService, - private selectableListService: SelectableListService + private selectableListService: SelectableListService, ) { } @@ -45,8 +62,8 @@ export class BulkAccessComponent implements OnInit { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generateIdListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + map((list: SelectableListState) => this.generateIdListBySelectedElements(list)), + ).subscribe(this.objectsSelected$), ); } @@ -74,12 +91,12 @@ export class BulkAccessComponent implements OnInit { const { file } = this.bulkAccessControlService.createPayloadFile({ bitstreamAccess, itemAccess, - state: settings.state + state: settings.state, }); this.bulkAccessControlService.executeScript( this.objectsSelected$.value || [], - file + file, ).subscribe(); } 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 index 14e0fdefb21..880e1f2472c 100644 --- 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 @@ -1,8 +1,13 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; + +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('BulkAccessSettingsComponent', () => { let component: BulkAccessSettingsComponent; @@ -15,36 +20,39 @@ describe('BulkAccessSettingsComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getFormValue: jasmine.createSpy('getFormValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [NgbAccordionModule, TranslateModule.forRoot()], - declarations: [BulkAccessSettingsComponent], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + imports: [NgbAccordionModule, TranslateModule.forRoot(), BulkAccessSettingsComponent], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(BulkAccessSettingsComponent, { + remove: { imports: [AccessControlFormContainerComponent] }, + }) + .compileComponents(); }); beforeEach(() => { 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 index eecc0162451..264cefc7084 100644 --- 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 @@ -1,13 +1,25 @@ -import { Component, ViewChild } from '@angular/core'; +import { NgIf } from '@angular/common'; import { - AccessControlFormContainerComponent -} from '../../../shared/access-control-form-container/access-control-form-container.component'; + Component, + ViewChild, +} from '@angular/core'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/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' + exportAs: 'dsBulkSettings', + imports: [ + NgbAccordionModule, + TranslateModule, + NgIf, + AccessControlFormContainerComponent, + ], + standalone: true, }) export class BulkAccessSettingsComponent { diff --git a/src/app/access-control/epeople-registry/epeople-registry.actions.ts b/src/app/access-control/epeople-registry/epeople-registry.actions.ts index a07ea37df29..e6e7608ba3f 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.actions.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.actions.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; + import { EPerson } from '../../core/eperson/models/eperson.model'; import { type } from '../../shared/ngrx/type'; 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 bf7b9a2060d..92968d2e28b 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -43,7 +43,7 @@ - diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts index 55d0508c103..524f3e31243 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts @@ -1,17 +1,21 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SharedModule } from '../../shared/shared.module'; -import { DeleteCommunityPageComponent } from './delete-community-page.component'; import { RequestService } from '../../core/data/request.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { DeleteCommunityPageComponent } from './delete-community-page.component'; describe('DeleteCommunityPageComponent', () => { let comp: DeleteCommunityPageComponent; @@ -19,16 +23,15 @@ describe('DeleteCommunityPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [DeleteCommunityPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, DeleteCommunityPageComponent], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: NotificationsService, useValue: {} }, - { provide: RequestService, useValue: {}} + { provide: RequestService, useValue: {} }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.ts b/src/app/community-page/delete-community-page/delete-community-page.component.ts index 65b7c81b38b..f35e2d6bd23 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.ts @@ -1,11 +1,23 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Community } from '../../core/shared/community.model'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { VarDirective } from '../../shared/utils/var.directive'; /** * Component that represents the page where a user can delete an existing Community @@ -13,7 +25,14 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-delete-community', styleUrls: ['./delete-community-page.component.scss'], - templateUrl: './delete-community-page.component.html' + templateUrl: './delete-community-page.component.html', + imports: [ + TranslateModule, + AsyncPipe, + VarDirective, + NgIf, + ], + standalone: true, }) export class DeleteCommunityPageComponent extends DeleteComColPageComponent { protected frontendURL = '/communities/'; diff --git a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts index d895cfd820b..28879ed7abf 100644 --- a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts @@ -1,25 +1,80 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { + of as observableOf, + of, +} from 'rxjs'; +import { Community } from '../../../core/shared/community.model'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { CommunityAccessControlComponent } from './community-access-control.component'; -xdescribe('CommunityAccessControlComponent', () => { +describe('CommunityAccessControlComponent', () => { let component: CommunityAccessControlComponent; let fixture: ComponentFixture; + const testCommunity = Object.assign(new Community(), + { + type: 'community', + metadata: { + 'dc.title': [{ value: 'community' }], + }, + uuid: 'communityUUID', + parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), + + _links: { + parentCommunity: 'site', + self: '/' + 'communityUUID', + }, + }, + ); + + const routeStub = { + parent: { + parent: { + data: of({ + dso: createSuccessfulRemoteDataObject(testCommunity), + }), + }, + }, + }; + + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ CommunityAccessControlComponent ] + imports: [CommunityAccessControlComponent], + providers: [{ + provide: ActivatedRoute, useValue: routeStub, + }], }) - .compileComponents(); + .overrideComponent(CommunityAccessControlComponent, { + remove: { + imports: [AccessControlFormContainerComponent], + }, + }) + .compileComponents(); }); + beforeEach(() => { fixture = TestBed.createComponent(CommunityAccessControlComponent); component = fixture.componentInstance; fixture.detectChanges(); + component.ngOnInit(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set itemRD$', (done) => { + component.itemRD$.subscribe(result => { + expect(result).toEqual(createSuccessfulRemoteDataObject(testCommunity)); + done(); + }); + }); }); diff --git a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts index 8a216e38dfa..a0e094e21d4 100644 --- a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts +++ b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts @@ -1,15 +1,30 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; + +import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; @Component({ selector: 'ds-community-access-control', templateUrl: './community-access-control.component.html', styleUrls: ['./community-access-control.component.scss'], + imports: [ + AccessControlFormContainerComponent, + NgIf, + AsyncPipe, + ], + standalone: true, }) export class CommunityAccessControlComponent implements OnInit { itemRD$: Observable>; @@ -18,7 +33,7 @@ export class CommunityAccessControlComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.parent.parent.data.pipe( - map((data) => data.dso) + map((data) => data.dso), ).pipe(getFirstSucceededRemoteData()) as Observable>; } } diff --git a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts index 719cf83a26f..921bbf0cfda 100644 --- a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts @@ -1,15 +1,22 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; - import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; +import { Collection } from '../../../core/shared/collection.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component'; import { CommunityAuthorizationsComponent } from './community-authorizations.component'; -import { Collection } from '../../../core/shared/collection.model'; describe('CommunityAuthorizationsComponent', () => { let comp: CommunityAuthorizationsComponent; @@ -19,8 +26,8 @@ describe('CommunityAuthorizationsComponent', () => { uuid: 'community', id: 'community', _links: { - self: { href: 'community-selflink' } - } + self: { href: 'community-selflink' }, + }, }); const communityRD = createSuccessfulRemoteDataObject(community); @@ -29,25 +36,31 @@ describe('CommunityAuthorizationsComponent', () => { parent: { parent: { data: observableOf({ - dso: communityRD - }) - } - } + dso: communityRD, + }), + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - CommonModule + CommonModule, + CommunityAuthorizationsComponent, ], - declarations: [CommunityAuthorizationsComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, ChangeDetectorRef, CommunityAuthorizationsComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CommunityAuthorizationsComponent, { + remove: { + imports: [ResourcePoliciesComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts index 7a9f224311d..3e42a830bef 100644 --- a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts +++ b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts @@ -1,13 +1,27 @@ -import { Component, OnInit } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { + first, + map, +} from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component'; @Component({ selector: 'ds-community-authorizations', templateUrl: './community-authorizations.component.html', + imports: [ + ResourcePoliciesComponent, + AsyncPipe, + ], + standalone: true, }) /** * Component that handles the community Authorizations @@ -25,7 +39,7 @@ export class CommunityAuthorizationsComponent impl * @param {ActivatedRoute} route */ constructor( - private route: ActivatedRoute + private route: ActivatedRoute, ) { } diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts index 1b1ee2c9f95..541308c9424 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts @@ -1,12 +1,21 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { ActivatedRoute } from '@angular/router'; + import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { CommunityCurateComponent } from './community-curate.component'; import { Community } from '../../../core/shared/community.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { CommunityCurateComponent } from './community-curate.component'; describe('CommunityCurateComponent', () => { let comp: CommunityCurateComponent; @@ -17,31 +26,36 @@ describe('CommunityCurateComponent', () => { let dsoNameService; const community = Object.assign(new Community(), { - metadata: {'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]} + metadata: { 'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1' }] }, }); beforeEach(waitForAsync(() => { routeStub = { parent: { data: observableOf({ - dso: createSuccessfulRemoteDataObject(community) - }) - } + dso: createSuccessfulRemoteDataObject(community), + }), + }, }; dsoNameService = jasmine.createSpyObj('dsoNameService', { - getName: 'Community Name' + getName: 'Community Name', }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [CommunityCurateComponent], + imports: [TranslateModule.forRoot(), CommunityCurateComponent], providers: [ - {provide: ActivatedRoute, useValue: routeStub}, - {provide: DSONameService, useValue: dsoNameService} + { provide: ActivatedRoute, useValue: routeStub }, + { provide: DSONameService, useValue: dsoNameService }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(CommunityCurateComponent, { + remove: { + imports: [CurationFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -58,7 +72,7 @@ describe('CommunityCurateComponent', () => { }); it('should contain the community information provided in the route', () => { comp.dsoRD$.subscribe((value) => { - expect(value.payload.handle + expect(value.payload.handle, ).toEqual('123456789/1'); }); comp.communityName$.subscribe((value) => { diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts index 8ae04af8f15..fd4d2408278 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts @@ -1,10 +1,21 @@ -import { Component, OnInit } from '@angular/core'; -import { Community } from '../../../core/shared/community.model'; +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { filter, map, take } from 'rxjs/operators'; -import { RemoteData } from '../../../core/data/remote-data'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; +import { + filter, + map, + take, +} from 'rxjs/operators'; + import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; import { hasValue } from '../../../shared/empty.util'; /** @@ -13,6 +24,12 @@ import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-community-curate', templateUrl: './community-curate.component.html', + imports: [ + CurationFormComponent, + TranslateModule, + AsyncPipe, + ], + standalone: true, }) export class CommunityCurateComponent implements OnInit { @@ -35,7 +52,7 @@ export class CommunityCurateComponent implements OnInit { filter((rd: RemoteData) => hasValue(rd)), map((rd: RemoteData) => { return this.dsoNameService.getName(rd.payload); - }) + }), ); } diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts index c597fac0bd3..b82beaa3f73 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts @@ -1,15 +1,20 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { SharedModule } from '../../../shared/shared.module'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CommunityMetadataComponent } from './community-metadata.component'; + import { CommunityDataService } from '../../../core/data/community-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; +import { CommunityMetadataComponent } from './community-metadata.component'; describe('CommunityMetadataComponent', () => { let comp: CommunityMetadataComponent; @@ -17,15 +22,20 @@ describe('CommunityMetadataComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CommunityMetadataComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CommunityMetadataComponent], providers: [ { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(CommunityMetadataComponent, { + remove: { + imports: [CommunityFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts index a2dbfa6eb61..8001bd29697 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -1,10 +1,16 @@ +import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; -import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Community } from '../../../core/shared/community.model'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + import { CommunityDataService } from '../../../core/data/community-data.service'; +import { Community } from '../../../core/shared/community.model'; +import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; /** * Component for editing a community's metadata @@ -12,6 +18,11 @@ import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-community-metadata', templateUrl: './community-metadata.component.html', + imports: [ + CommunityFormComponent, + AsyncPipe, + ], + standalone: true, }) export class CommunityMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/communities/'; @@ -22,7 +33,7 @@ export class CommunityMetadataComponent extends ComcolMetadataComponent { @@ -39,10 +47,10 @@ describe('CommunityRolesComponent', () => { href: 'adminGroup link', }, }, - }) + }), ), - }) - } + }), + }, }; const requestService = { @@ -55,13 +63,9 @@ describe('CommunityRolesComponent', () => { TestBed.configureTestingModule({ imports: [ - ComcolModule, - SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), - NoopAnimationsModule - ], - declarations: [ + NoopAnimationsModule, CommunityRolesComponent, ], providers: [ @@ -69,9 +73,9 @@ describe('CommunityRolesComponent', () => { { provide: ActivatedRoute, useValue: route }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, - { provide: NotificationsService, useClass: NotificationsServiceStub } + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(CommunityRolesComponent); diff --git a/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts index 9468aa7048b..2e85cbe4c36 100644 --- a/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts +++ b/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts @@ -1,11 +1,26 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgForOf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; -import { Community } from '../../../core/shared/community.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { + first, + map, +} from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; import { HALLink } from '../../../core/shared/hal-link.model'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../../../core/shared/operators'; +import { ComcolRoleComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; /** * Component for managing a community's roles @@ -13,6 +28,12 @@ import { HALLink } from '../../../core/shared/hal-link.model'; @Component({ selector: 'ds-community-roles', templateUrl: './community-roles.component.html', + imports: [ + ComcolRoleComponent, + AsyncPipe, + NgForOf, + ], + standalone: true, }) export class CommunityRolesComponent implements OnInit { diff --git a/src/app/community-page/edit-community-page/edit-community-page-routes.ts b/src/app/community-page/edit-community-page/edit-community-page-routes.ts new file mode 100644 index 00000000000..a15312a216e --- /dev/null +++ b/src/app/community-page/edit-community-page/edit-community-page-routes.ts @@ -0,0 +1,91 @@ +import { + mapToCanActivate, + Route, +} from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; +import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; +import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; +import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; +import { EditCommunityPageComponent } from './edit-community-page.component'; + +/** + * Routing module that handles the routing for the Edit Community page administrator functionality + */ + +export const ROUTES: Route[] = [ + { + path: '', + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'community.edit' }, + component: EditCommunityPageComponent, + canActivate: mapToCanActivate([CommunityAdministratorGuard]), + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full', + }, + { + path: 'metadata', + component: CommunityMetadataComponent, + data: { + title: 'community.edit.tabs.metadata.title', + hideReturnButton: true, + showBreadcrumbs: true, + }, + }, + { + path: 'roles', + component: CommunityRolesComponent, + data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }, + }, + { + path: 'curate', + component: CommunityCurateComponent, + data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }, + }, + { + path: 'access-control', + component: CommunityAccessControlComponent, + data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true }, + }, + { + path: 'authorizations', + data: { showBreadcrumbs: true }, + children: [ + { + path: 'create', + resolve: { + resourcePolicyTarget: resourcePolicyTargetResolver, + }, + component: ResourcePolicyCreateComponent, + data: { title: 'resource-policies.create.page.title' }, + }, + { + path: 'edit', + resolve: { + resourcePolicy: resourcePolicyResolver, + }, + component: ResourcePolicyEditComponent, + data: { title: 'resource-policies.edit.page.title' }, + }, + { + path: '', + component: CommunityAuthorizationsComponent, + data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true, hideReturnButton: true }, + }, + ], + }, + ], + }, +]; diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts index 3a4c3351c32..f099f6fc2af 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts @@ -1,13 +1,17 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { SharedModule } from '../../shared/shared.module'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { EditCommunityPageComponent } from './edit-community-page.component'; + import { CommunityDataService } from '../../core/data/community-data.service'; +import { EditCommunityPageComponent } from './edit-community-page.component'; describe('EditCommunityPageComponent', () => { let comp: EditCommunityPageComponent; @@ -15,36 +19,35 @@ describe('EditCommunityPageComponent', () => { const routeStub = { data: observableOf({ - dso: { payload: {} } + dso: { payload: {} }, }), routeConfig: { children: [ { path: 'mockUrl', data: { - hideReturnButton: false - } - } - ] + hideReturnButton: false, + }, + }, + ], }, snapshot: { firstChild: { routeConfig: { - path: 'mockUrl' - } - } - } + path: 'mockUrl', + }, + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCommunityPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, EditCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: routeStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.ts b/src/app/community-page/edit-community-page/edit-community-page.component.ts index 54a6ee49442..194976c3a8d 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.ts @@ -1,6 +1,19 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; +import { + ActivatedRoute, + Router, + RouterLink, + RouterOutlet, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + import { Community } from '../../core/shared/community.model'; -import { ActivatedRoute, Router } from '@angular/router'; import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { getCommunityPageRoute } from '../community-page-routing-paths'; @@ -9,14 +22,24 @@ import { getCommunityPageRoute } from '../community-page-routing-paths'; */ @Component({ selector: 'ds-edit-community', - templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' + templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html', + standalone: true, + imports: [ + RouterLink, + TranslateModule, + NgClass, + NgForOf, + RouterOutlet, + NgIf, + AsyncPipe, + ], }) export class EditCommunityPageComponent extends EditComColPageComponent { type = 'community'; public constructor( protected router: Router, - protected route: ActivatedRoute + protected route: ActivatedRoute, ) { super(router, route); } diff --git a/src/app/community-page/edit-community-page/edit-community-page.module.ts b/src/app/community-page/edit-community-page/edit-community-page.module.ts deleted file mode 100644 index 5190d6a0083..00000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.module.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '../../shared/shared.module'; -import { EditCommunityPageRoutingModule } from './edit-community-page.routing.module'; -import { EditCommunityPageComponent } from './edit-community-page.component'; -import { CommunityCurateComponent } from './community-curate/community-curate.component'; -import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; -import { CommunityRolesComponent } from './community-roles/community-roles.component'; -import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { CommunityFormModule } from '../community-form/community-form.module'; -import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; -import { ComcolModule } from '../../shared/comcol/comcol.module'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; -import { - AccessControlFormModule -} from '../../shared/access-control-form-container/access-control-form.module'; - -/** - * Module that contains all components related to the Edit Community page administrator functionality - */ -@NgModule({ - imports: [ - CommonModule, - SharedModule, - EditCommunityPageRoutingModule, - CommunityFormModule, - ComcolModule, - ResourcePoliciesModule, - AccessControlFormModule, - ], - declarations: [ - EditCommunityPageComponent, - CommunityCurateComponent, - CommunityMetadataComponent, - CommunityRolesComponent, - CommunityAuthorizationsComponent, - CommunityAccessControlComponent - ] -}) -export class EditCommunityPageModule { - -} diff --git a/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts deleted file mode 100644 index 994c6b5e961..00000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { EditCommunityPageComponent } from './edit-community-page.component'; -import { RouterModule } from '@angular/router'; -import { NgModule } from '@angular/core'; -import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; -import { CommunityRolesComponent } from './community-roles/community-roles.component'; -import { CommunityCurateComponent } from './community-curate/community-curate.component'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; -import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; - -/** - * Routing module that handles the routing for the Edit Community page administrator functionality - */ -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { breadcrumbKey: 'community.edit' }, - component: EditCommunityPageComponent, - canActivate: [CommunityAdministratorGuard], - children: [ - { - path: '', - redirectTo: 'metadata', - pathMatch: 'full' - }, - { - path: 'metadata', - component: CommunityMetadataComponent, - data: { - title: 'community.edit.tabs.metadata.title', - hideReturnButton: true, - showBreadcrumbs: true - } - }, - { - path: 'roles', - component: CommunityRolesComponent, - data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true } - }, - { - path: 'curate', - component: CommunityCurateComponent, - data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true } - }, - { - path: 'access-control', - component: CommunityAccessControlComponent, - data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true } - }, - /*{ - path: 'authorizations', - component: CommunityAuthorizationsComponent, - data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true } - },*/ - { - path: 'authorizations', - data: { showBreadcrumbs: true }, - children: [ - { - path: 'create', - resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver - }, - component: ResourcePolicyCreateComponent, - data: { title: 'resource-policies.create.page.title' } - }, - { - path: 'edit', - resolve: { - resourcePolicy: ResourcePolicyResolver - }, - component: ResourcePolicyEditComponent, - data: { title: 'resource-policies.edit.page.title' } - }, - { - path: '', - component: CommunityAuthorizationsComponent, - data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true, hideReturnButton: true } - } - ] - } - ] - } - ]) - ], - providers: [ - ResourcePolicyResolver, - ResourcePolicyTargetResolver - ] -}) -export class EditCommunityPageRoutingModule { - -} diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 8c4af30991b..dc4ab520812 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -1,36 +1,41 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - +import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; -import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; -import { Community } from '../../../../core/shared/community.model'; -import { SharedModule } from '../../../../shared/shared.module'; import { CollectionDataService } from '../../../../core/data/collection-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { FindListOptions } from '../../../../core/data/find-list-options.model'; import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { LinkHeadService } from '../../../../core/services/link-head.service'; +import { Community } from '../../../../core/shared/community.model'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { HostWindowService } from '../../../../shared/host-window.service'; -import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; -import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; -import { ThemeService } from '../../../../shared/theme-support/theme.service'; +import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../../../core/data/find-list-options.model'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { LinkHeadService } from '../../../../core/services/link-head.service'; -import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; -import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; -import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; -import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { ThemeService } from '../../../../shared/theme-support/theme.service'; +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; -describe('CommunityPageSubCollectionList Component', () => { +describe('CommunityPageSubCollectionListComponent', () => { let comp: CommunityPageSubCollectionListComponent; let fixture: ComponentFixture; let collectionDataServiceStub: any; @@ -41,67 +46,67 @@ describe('CommunityPageSubCollectionList Component', () => { id: '123456789-1', metadata: { 'dc.title': [ - { language: 'en_US', value: 'Collection 1' } - ] - } + { language: 'en_US', value: 'Collection 1' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 2' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 3' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-4', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 4' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 5' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 6' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 7' }, + ], + }, }), - Object.assign(new Community(), { - id: '123456789-2', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 2' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-3', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 3' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-4', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 4' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-5', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 5' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-6', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 6' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-7', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 7' } - ] - } - }) ]; const mockCommunity = Object.assign(new Community(), { id: '123456789', metadata: { 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - } + { language: 'en_US', value: 'Test title' }, + ], + }, }); collectionDataServiceStub = { @@ -119,7 +124,7 @@ describe('CommunityPageSubCollectionList Component', () => { } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); - } + }, }; const paginationService = new PaginationServiceStub(); @@ -127,7 +132,7 @@ describe('CommunityPageSubCollectionList Component', () => { themeService = getMockThemeService(); const linkHeadService = jasmine.createSpyObj('linkHeadService', { - addTag: '' + addTag: '', }); const groupDataService = jasmine.createSpyObj('groupsDataService', { @@ -140,21 +145,20 @@ describe('CommunityPageSubCollectionList Component', () => { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { name: 'test', values: [ - 'org.dspace.ctask.general.ProfileFormats = test' - ] - })) + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - SharedModule, RouterTestingModule.withRoutes([]), NgbModule, - NoopAnimationsModule + NoopAnimationsModule, + CommunityPageSubCollectionListComponent, ], - declarations: [CommunityPageSubCollectionListComponent], providers: [ { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, @@ -166,7 +170,7 @@ describe('CommunityPageSubCollectionList Component', () => { { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -177,19 +181,19 @@ describe('CommunityPageSubCollectionList Component', () => { }); - it('should display a list of collections', () => { - waitForAsync(() => { - subCollList = collections; - fixture.detectChanges(); + it('should display a list of collections', async () => { + subCollList = collections; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(5); - expect(collList[0].nativeElement.textContent).toContain('Collection 1'); - expect(collList[1].nativeElement.textContent).toContain('Collection 2'); - expect(collList[2].nativeElement.textContent).toContain('Collection 3'); - expect(collList[3].nativeElement.textContent).toContain('Collection 4'); - expect(collList[4].nativeElement.textContent).toContain('Collection 5'); - }); + const collList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); }); it('should not display the header when list of collections is empty', () => { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index 92e689b127f..2935f255958 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,23 +1,55 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + SortDirection, + SortOptions, +} from '../../../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../../../core/data/collection-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; import { Collection } from '../../../../core/shared/collection.model'; import { Community } from '../../../../core/shared/community.model'; import { fadeIn } from '../../../../shared/animations/fade'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; -import { CollectionDataService } from '../../../../core/data/collection-data.service'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { switchMap } from 'rxjs/operators'; import { hasValue } from '../../../../shared/empty.util'; +import { ErrorComponent } from '../../../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component'; +import { ObjectCollectionComponent } from '../../../../shared/object-collection/object-collection.component'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ selector: 'ds-community-page-sub-collection-list', styleUrls: ['./community-page-sub-collection-list.component.scss'], templateUrl: './community-page-sub-collection-list.component.html', - animations:[fadeIn] + animations: [fadeIn], + imports: [ + ObjectCollectionComponent, + ErrorComponent, + ThemedLoadingComponent, + NgIf, + TranslateModule, + AsyncPipe, + VarDirective, + ], + standalone: true, }) export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy { @Input() community: Community; @@ -74,17 +106,17 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro * Initialise the list of collections */ initPage() { - const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); - const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); + const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); + const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe( switchMap(([currentPagination, currentSort]) => { return this.cds.findByParent(this.community.id, { currentPage: currentPagination.currentPage, elementsPerPage: currentPagination.pageSize, - sort: {field: currentSort.field, direction: currentSort.direction} + sort: { field: currentSort.field, direction: currentSort.direction }, }); - }) + }), ).subscribe((results) => { this.subCollectionsRDObs.next(results); })); diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index ebbec33e8e9..ff5d057b312 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts @@ -1,12 +1,17 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { Community } from '../../../../core/shared/community.model'; import { ThemedComponent } from '../../../../shared/theme-support/themed.component'; import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; -import { Component, Input } from '@angular/core'; -import { Community } from '../../../../core/shared/community.model'; @Component({ selector: 'ds-themed-community-page-sub-collection-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts index cb3c41aa97d..85d8eb4fb70 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -1,7 +1,11 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SubComColSectionComponent } from './sub-com-col-section.component'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; + import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { SubComColSectionComponent } from './sub-com-col-section.component'; describe('SubComColSectionComponent', () => { let component: SubComColSectionComponent; @@ -14,9 +18,7 @@ describe('SubComColSectionComponent', () => { activatedRoute.parent = new ActivatedRouteStub(); await TestBed.configureTestingModule({ - declarations: [ - SubComColSectionComponent, - ], + imports: [SubComColSectionComponent], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, ], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts index a72674adec3..7aed3be076b 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -1,14 +1,34 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Data, +} from '@angular/router'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; -import { ActivatedRoute, Data } from '@angular/router'; -import { map } from 'rxjs/operators'; +import { ThemedCollectionPageSubCollectionListComponent } from './sub-collection-list/themed-community-page-sub-collection-list.component'; +import { ThemedCommunityPageSubCommunityListComponent } from './sub-community-list/themed-community-page-sub-community-list.component'; @Component({ selector: 'ds-sub-com-col-section', templateUrl: './sub-com-col-section.component.html', styleUrls: ['./sub-com-col-section.component.scss'], + imports: [ + ThemedCommunityPageSubCommunityListComponent, + ThemedCollectionPageSubCollectionListComponent, + AsyncPipe, + NgIf, + ], + standalone: true, }) export class SubComColSectionComponent implements OnInit { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts index c5efc9c2c1c..2654585eda9 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,36 +1,41 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; - +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; -import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; -import { Community } from '../../../../core/shared/community.model'; -import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { SharedModule } from '../../../../shared/shared.module'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { HostWindowService } from '../../../../shared/host-window.service'; -import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; import { CommunityDataService } from '../../../../core/data/community-data.service'; -import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; -import { ThemeService } from '../../../../shared/theme-support/theme.service'; -import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { FindListOptions } from '../../../../core/data/find-list-options.model'; +import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; import { LinkHeadService } from '../../../../core/services/link-head.service'; -import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { Community } from '../../../../core/shared/community.model'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { HostWindowService } from '../../../../shared/host-window.service'; +import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; +import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; -import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { ThemeService } from '../../../../shared/theme-support/theme.service'; +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; -describe('CommunityPageSubCommunityListComponent Component', () => { +describe('CommunityPageSubCommunityListComponent', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; @@ -41,67 +46,67 @@ describe('CommunityPageSubCommunityListComponent Component', () => { id: '123456789-1', metadata: { 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 1' } - ] - } + { language: 'en_US', value: 'SubCommunity 1' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 2' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' }, + ], + }, + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' }, + ], + }, }), - Object.assign(new Community(), { - id: '123456789-2', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 2' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-3', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 3' } - ] - } - }), - Object.assign(new Community(), { - id: '12345678942', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 4' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-5', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 5' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-6', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 6' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-7', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 7' } - ] - } - }) ]; const mockCommunity = Object.assign(new Community(), { id: '123456789', metadata: { 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - } + { language: 'en_US', value: 'Test title' }, + ], + }, }); communityDataServiceStub = { @@ -120,11 +125,11 @@ describe('CommunityPageSubCommunityListComponent Component', () => { } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); - } + }, }; const linkHeadService = jasmine.createSpyObj('linkHeadService', { - addTag: '' + addTag: '', }); const groupDataService = jasmine.createSpyObj('groupsDataService', { @@ -137,9 +142,9 @@ describe('CommunityPageSubCommunityListComponent Component', () => { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { name: 'test', values: [ - 'org.dspace.ctask.general.ProfileFormats = test' - ] - })) + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), }); const paginationService = new PaginationServiceStub(); @@ -150,12 +155,11 @@ describe('CommunityPageSubCommunityListComponent Component', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - SharedModule, RouterTestingModule.withRoutes([]), NgbModule, - NoopAnimationsModule + NoopAnimationsModule, + CommunityPageSubCommunityListComponent, ], - declarations: [CommunityPageSubCommunityListComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, @@ -167,7 +171,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => { { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -179,19 +183,19 @@ describe('CommunityPageSubCommunityListComponent Component', () => { }); - it('should display a list of sub-communities', () => { - waitForAsync(() => { - subCommList = subcommunities; - fixture.detectChanges(); + it('should display a list of sub-communities', async () => { + subCommList = subcommunities; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); - const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(5); - expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); - expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); - expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); - expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); - expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); - }); + const subComList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); it('should not display the header when list of sub-communities is empty', () => { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 3108be8a601..4f74eff601b 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -1,24 +1,58 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; - +import { + SortDirection, + SortOptions, +} from '../../../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; import { Community } from '../../../../core/shared/community.model'; import { fadeIn } from '../../../../shared/animations/fade'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; -import { CommunityDataService } from '../../../../core/data/community-data.service'; -import { switchMap } from 'rxjs/operators'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { hasValue } from '../../../../shared/empty.util'; +import { ErrorComponent } from '../../../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component'; +import { ObjectCollectionComponent } from '../../../../shared/object-collection/object-collection.component'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ selector: 'ds-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', - animations: [fadeIn] + animations: [fadeIn], + imports: [ + ErrorComponent, + ThemedLoadingComponent, + VarDirective, + NgIf, + ObjectCollectionComponent, + AsyncPipe, + TranslateModule, + ObjectCollectionComponent, + ErrorComponent, + ThemedLoadingComponent, + VarDirective, + ], + standalone: true, }) /** * Component to render the sub-communities of a Community @@ -86,9 +120,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy return this.cds.findByParent(this.community.id, { currentPage: currentPagination.currentPage, elementsPerPage: currentPagination.pageSize, - sort: { field: currentSort.field, direction: currentSort.direction } + sort: { field: currentSort.field, direction: currentSort.direction }, }); - }) + }), ).subscribe((results) => { this.subCommunitiesRDObs.next(results); })); diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 9c500cac10d..11b62d68e46 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts @@ -1,12 +1,17 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { Community } from '../../../../core/shared/community.model'; import { ThemedComponent } from '../../../../shared/theme-support/themed.component'; import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; -import { Component, Input } from '@angular/core'; -import { Community } from '../../../../core/shared/community.model'; @Component({ selector: 'ds-themed-community-page-sub-community-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { diff --git a/src/app/community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts index eeb058fb047..41a29607190 100644 --- a/src/app/community-page/themed-community-page.component.ts +++ b/src/app/community-page/themed-community-page.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../shared/theme-support/themed.component'; import { CommunityPageComponent } from './community-page.component'; @@ -9,6 +10,7 @@ import { CommunityPageComponent } from './community-page.component'; selector: 'ds-themed-community-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', + standalone: true, }) export class ThemedCommunityPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/core/auth/auth-blocking.guard.spec.ts b/src/app/core/auth/auth-blocking.guard.spec.ts index 3747edd532c..295e5b1e751 100644 --- a/src/app/core/auth/auth-blocking.guard.spec.ts +++ b/src/app/core/auth/auth-blocking.guard.spec.ts @@ -1,15 +1,26 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { Store, StoreModule } from '@ngrx/store'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; import { cold } from 'jasmine-marbles'; -import { AppState, storeModuleConfig } from '../../app.reducer'; -import { AuthBlockingGuard } from './auth-blocking.guard'; +import { + AppState, + storeModuleConfig, +} from '../../app.reducer'; import { authReducer } from './auth.reducer'; +import { authBlockingGuard } from './auth-blocking.guard'; -describe('AuthBlockingGuard', () => { - let guard: AuthBlockingGuard; +describe('authBlockingGuard', () => { + let guard: any; let initialState; let store: Store; let mockStore: MockStore; @@ -21,9 +32,9 @@ describe('AuthBlockingGuard', () => { loaded: false, blocking: undefined, loading: false, - authMethods: [] - } - } + authMethods: [], + }, + }, }; beforeEach(waitForAsync(() => { @@ -33,22 +44,22 @@ describe('AuthBlockingGuard', () => { ], providers: [ provideMockStore({ initialState }), - { provide: AuthBlockingGuard, useValue: guard } - ] + { provide: authBlockingGuard, useValue: guard }, + ], }).compileComponents(); })); beforeEach(() => { store = TestBed.inject(Store); mockStore = store as MockStore; - guard = new AuthBlockingGuard(store); + guard = authBlockingGuard; }); describe(`canActivate`, () => { describe(`when authState.blocking is undefined`, () => { it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -58,15 +69,15 @@ describe('AuthBlockingGuard', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'auth': { - blocking: true - } - }) + blocking: true, + }, + }), }); mockStore.setState(state); }); it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -76,15 +87,15 @@ describe('AuthBlockingGuard', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'auth': { - blocking: false - } - }) + blocking: false, + }, + }), }); mockStore.setState(state); }); it(`should succeed`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); + expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true })); done(); }); }); diff --git a/src/app/core/auth/auth-blocking.guard.ts b/src/app/core/auth/auth-blocking.guard.ts index 9054f66f8b1..c76480ec0d2 100644 --- a/src/app/core/auth/auth-blocking.guard.ts +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -1,8 +1,21 @@ -import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; -import { select, Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; +import { + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, + take, +} from 'rxjs/operators'; + import { AppState } from '../../app.reducer'; import { isAuthenticationBlocking } from './selectors'; @@ -11,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors'; * route until the authentication status has loaded. * To ensure all rest requests get the correct auth header. */ -@Injectable({ - providedIn: 'root' -}) -export class AuthBlockingGuard implements CanActivate { - - constructor(private store: Store) { - } - - /** - * True when the authentication isn't blocking everything - */ - canActivate(): Observable { - return this.store.pipe(select(isAuthenticationBlocking)).pipe( - map((isBlocking: boolean) => isBlocking === false), - distinctUntilChanged(), - filter((finished: boolean) => finished === true), - take(1), - ); - } +export const authBlockingGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + store: Store = inject(Store), +): Observable => { + return store.pipe(select(isAuthenticationBlocking)).pipe( + map((isBlocking: boolean) => isBlocking === false), + distinctUntilChanged(), + filter((finished: boolean) => finished === true), + take(1), + ); +}; -} diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts index 063aad612f1..2220efe5faa 100644 --- a/src/app/core/auth/auth-request.service.spec.ts +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -1,17 +1,21 @@ -import { AuthRequestService } from './auth-request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { PostRequest } from '../data/request.models'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; -import { ShortLivedToken } from './models/short-lived-token.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; +import { PostRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { RestRequestMethod } from '../data/rest-request-method'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import objectContaining = jasmine.objectContaining; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { Observable, of as observableOf } from 'rxjs'; +import { ShortLivedToken } from './models/short-lived-token.model'; +import objectContaining = jasmine.objectContaining; describe(`AuthRequestService`, () => { let halService: HALEndpointService; @@ -30,7 +34,7 @@ describe(`AuthRequestService`, () => { constructor( hes: HALEndpointService, rs: RequestService, - rdbs: RemoteDataBuildService + rdbs: RemoteDataBuildService, ) { super(hes, rs, rdbs); } @@ -44,19 +48,19 @@ describe(`AuthRequestService`, () => { endpointURL = 'https://rest.api/auth'; requestID = 'requestID'; shortLivedToken = Object.assign(new ShortLivedToken(), { - value: 'some-token' + value: 'some-token', }); shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken); halService = jasmine.createSpyObj('halService', { - 'getEndpoint': cold('a', { a: endpointURL }) + 'getEndpoint': cold('a', { a: endpointURL }), }); requestService = jasmine.createSpyObj('requestService', { 'generateRequestId': requestID, 'send': null, }); rdbService = jasmine.createSpyObj('rdbService', { - 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }) + 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }), }); service = new TestAuthRequestService(halService, requestService, rdbService); diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 7c1f17dec23..5d11b9f4cb0 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,18 +1,29 @@ import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, switchMap, tap, take } from 'rxjs/operators'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; +import { + distinctUntilChanged, + filter, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + import { isNotEmpty } from '../../shared/empty.util'; -import { GetRequest, PostRequest, } from '../data/request.models'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; +import { + GetRequest, + PostRequest, +} from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { RestRequest } from '../data/rest-request.model'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { URLCombiner } from '../url-combiner/url-combiner'; import { AuthStatus } from './models/auth-status.model'; import { ShortLivedToken } from './models/short-lived-token.model'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { RestRequest } from '../data/rest-request.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * Abstract service to send authentication requests @@ -23,8 +34,8 @@ export abstract class AuthRequestService { constructor(protected halService: HALEndpointService, protected requestService: RequestService, - private rdbService: RemoteDataBuildService - ) { + private rdbService: RemoteDataBuildService, + ) { } /** @@ -65,7 +76,7 @@ export abstract class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), map((endpointURL: string) => new PostRequest(requestId, endpointURL, body, options)), - take(1) + take(1), ).subscribe((request: PostRequest) => { this.requestService.send(request); }); @@ -90,7 +101,7 @@ export abstract class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)), distinctUntilChanged(), map((endpointURL: string) => new GetRequest(requestId, endpointURL, undefined, options)), - take(1) + take(1), ).subscribe((request: GetRequest) => { this.requestService.send(request); }); @@ -125,7 +136,7 @@ export abstract class AuthRequestService { } else { return null; } - }) + }), ); } } diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 6bc4565682a..03b6bc11910 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -1,12 +1,13 @@ /* eslint-disable max-classes-per-file */ // import @ngrx import { Action } from '@ngrx/store'; + // import type function import { type } from '../../shared/ngrx/type'; -// import models -import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthStatus } from './models/auth-status.model'; +// import models +import { AuthTokenInfo } from './models/auth-token-info.model'; export const AuthActionTypes = { AUTHENTICATE: type('dspace/auth/AUTHENTICATE'), @@ -38,7 +39,7 @@ export const AuthActionTypes = { RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'), REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'), SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'), - UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE') + UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE'), }; @@ -426,6 +427,15 @@ export class UnsetUserAsIdleAction implements Action { public type: string = AuthActionTypes.UNSET_USER_AS_IDLE; } +/** + * Authentication error actions that include Error payloads. + */ +export type AuthErrorActionsWithErrorPayload + = AuthenticatedErrorAction + | AuthenticationErrorAction + | LogOutErrorAction + | RetrieveAuthenticatedEpersonErrorAction; + /** * Actions type. * @type {AuthActions} @@ -433,9 +443,7 @@ export class UnsetUserAsIdleAction implements Action { export type AuthActions = AuthenticateAction | AuthenticatedAction - | AuthenticatedErrorAction | AuthenticatedSuccessAction - | AuthenticationErrorAction | AuthenticationSuccessAction | CheckAuthenticationTokenAction | CheckAuthenticationTokenCookieAction @@ -452,10 +460,9 @@ export type AuthActions | RetrieveAuthMethodsErrorAction | RetrieveTokenAction | RetrieveAuthenticatedEpersonAction - | RetrieveAuthenticatedEpersonErrorAction | RetrieveAuthenticatedEpersonSuccessAction | SetRedirectUrlAction | RedirectAfterLoginSuccessAction | SetUserAsIdleAction - | UnsetUserAsIdleAction; - + | UnsetUserAsIdleAction + | AuthErrorActionsWithErrorPayload; diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 2e6ba917aae..a423455594a 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -1,12 +1,38 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; - +import { + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { cold, hot } from 'jasmine-marbles'; -import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, + throwError as observableThrow, +} from 'rxjs'; -import { AuthEffects } from './auth.effects'; +import { + AppState, + storeModuleConfig, +} from '../../app.reducer'; +import { + authMethodsMock, + AuthServiceStub, +} from '../../shared/testing/auth-service.stub'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { StoreActionTypes } from '../../store.actions'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AuthActionTypes, AuthenticatedAction, @@ -25,17 +51,16 @@ import { RetrieveAuthMethodsAction, RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, - RetrieveTokenAction + RetrieveTokenAction, } from './auth.actions'; -import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub'; -import { AuthService } from './auth.service'; +import { AuthEffects } from './auth.effects'; import { authReducer } from './auth.reducer'; +import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; -import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AppState, storeModuleConfig } from '../../app.reducer'; -import { StoreActionTypes } from '../../store.actions'; -import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { + isAuthenticated, + isAuthenticatedLoaded, +} from './selectors'; describe('AuthEffects', () => { let authEffects: AuthEffects; @@ -56,9 +81,9 @@ describe('AuthEffects', () => { authenticated: false, loaded: false, loading: false, - authMethods: [] - } - } + authMethods: [], + }, + }, }; } @@ -66,7 +91,7 @@ describe('AuthEffects', () => { init(); TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({ auth: authReducer }, storeModuleConfig) + StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), ], providers: [ AuthEffects, @@ -88,8 +113,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATE, - payload: { email: 'user', password: 'password' } - } + payload: { email: 'user', password: 'password' }, + }, }); const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) }); @@ -105,8 +130,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATE, - payload: { email: 'user', password: 'wrongpassword' } - } + payload: { email: 'user', password: 'wrongpassword' }, + }, }); const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) }); @@ -161,9 +186,9 @@ describe('AuthEffects', () => { type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: { authenticated: true, authToken: token, - userHref: EPersonMock._links.self.href - } - } + userHref: EPersonMock._links.self.href, + }, + }, }); const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) }); @@ -211,8 +236,8 @@ describe('AuthEffects', () => { spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( observableOf( { - authenticated: true - }) + authenticated: true, + }), ); spyOn((authEffects as any).authService, 'setExternalAuthStatus'); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); @@ -230,7 +255,7 @@ describe('AuthEffects', () => { it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => { spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( observableOf( - { authenticated: false }) + { authenticated: false }), ); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); @@ -260,8 +285,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON, - payload: EPersonMock._links.self.href - } + payload: EPersonMock._links.self.href, + }, }); const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) }); @@ -314,8 +339,8 @@ describe('AuthEffects', () => { it('should return a AUTHENTICATE_SUCCESS action in response to a RETRIEVE_TOKEN action', () => { actions = hot('--a-', { a: { - type: AuthActionTypes.RETRIEVE_TOKEN - } + type: AuthActionTypes.RETRIEVE_TOKEN, + }, }); const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) }); @@ -330,8 +355,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { - type: AuthActionTypes.RETRIEVE_TOKEN - } + type: AuthActionTypes.RETRIEVE_TOKEN, + }, }); const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 281355b769e..2919a40fa85 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,27 +1,47 @@ -import { Injectable, NgZone } from '@angular/core'; - +import { + Injectable, + NgZone, + Type, +} from '@angular/core'; +// import @ngrx +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { + Action, + select, + Store, +} from '@ngrx/store'; import { asyncScheduler, combineLatest as observableCombineLatest, Observable, of as observableOf, queueScheduler, - timer + timer, } from 'rxjs'; -import { catchError, filter, map, observeOn, switchMap, take, tap } from 'rxjs/operators'; -// import @ngrx -import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { Action, select, Store } from '@ngrx/store'; +import { + catchError, + filter, + map, + observeOn, + switchMap, + take, + tap, +} from 'rxjs/operators'; -// import services -import { AuthService } from './auth.service'; -import { EPerson } from '../eperson/models/eperson.model'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; +import { environment } from '../../../environments/environment'; import { AppState } from '../../app.reducer'; -import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions'; import { StoreActionTypes } from '../../store.actions'; -import { AuthMethod } from './models/auth.method'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { RequestActionTypes } from '../data/request.actions'; +import { EPerson } from '../eperson/models/eperson.model'; +import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler'; +import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler'; // import actions import { AuthActionTypes, @@ -31,6 +51,7 @@ import { AuthenticatedSuccessAction, AuthenticationErrorAction, AuthenticationSuccessAction, + AuthErrorActionsWithErrorPayload, CheckAuthenticationTokenCookieAction, LogOutErrorAction, LogOutSuccessAction, @@ -45,20 +66,32 @@ import { RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, RetrieveTokenAction, - SetUserAsIdleAction + SetUserAsIdleAction, } from './auth.actions'; -import { hasValue } from '../../shared/empty.util'; -import { environment } from '../../../environments/environment'; -import { RequestActionTypes } from '../data/request.actions'; -import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions'; -import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler'; -import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +// import services +import { AuthService } from './auth.service'; +import { AuthMethod } from './models/auth.method'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; +import { + isAuthenticated, + isAuthenticatedLoaded, +} from './selectors'; // Action Types that do not break/prevent the user from an idle state const IDLE_TIMER_IGNORE_TYPES: string[] = [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE), - ...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)]; + ...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)]; + +export function errorToAuthAction$(actionType: Type, error: unknown): Observable { + if (error instanceof Error) { + return observableOf(new actionType(error)); + } + + // If we caught something that's not an Error: complain & drop type safety + console.warn('AuthEffects caught non-Error object:', error); + return observableOf(new actionType(error as any)); +} @Injectable() export class AuthEffects { @@ -73,14 +106,14 @@ export class AuthEffects { return this.authService.authenticate(action.payload.email, action.payload.password).pipe( take(1), map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)), - catchError((error) => observableOf(new AuthenticationErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticationErrorAction, error)), ); - }) + }), )); public authenticateSuccess$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), - map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) + map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)), )); public authenticated$: Observable = createEffect(() => this.actions$.pipe( @@ -88,8 +121,9 @@ export class AuthEffects { switchMap((action: AuthenticatedAction) => { return this.authService.authenticatedUser(action.payload).pipe( map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)), - catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); - }) + catchError((error: unknown) => errorToAuthAction$(AuthenticatedErrorAction, error)), + ); + }), )); public authenticatedSuccess$: Observable = createEffect(() => this.actions$.pipe( @@ -97,7 +131,7 @@ export class AuthEffects { tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe( take(1), - map((redirectUrl: string) => [action, redirectUrl]) + map((redirectUrl: string) => [action, redirectUrl]), )), map(([action, redirectUrl]: [AuthenticatedSuccessAction, string]) => { if (hasValue(redirectUrl)) { @@ -105,7 +139,7 @@ export class AuthEffects { } else { return new RetrieveAuthenticatedEpersonAction(action.payload.userHref); } - }) + }), )); public redirectAfterLoginSuccess$: Observable = createEffect(() => this.actions$.pipe( @@ -113,13 +147,13 @@ export class AuthEffects { tap((action: RedirectAfterLoginSuccessAction) => { this.authService.clearRedirectUrl(); this.authService.navigateToRedirectUrl(action.payload); - }) + }), ), { dispatch: false }); // It means "reacts to this action but don't send another" public authenticatedError$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATED_ERROR), - tap((action: LogOutSuccessAction) => this.authService.removeToken()) + tap((action: LogOutSuccessAction) => this.authService.removeToken()), ), { dispatch: false }); public retrieveAuthenticatedEperson$: Observable = createEffect(() => this.actions$.pipe( @@ -134,17 +168,18 @@ export class AuthEffects { } return user$.pipe( map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)), - catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error)))); - }) + catchError((error: unknown) => errorToAuthAction$(RetrieveAuthenticatedEpersonErrorAction, error)), + ); + }), )); public checkToken$: Observable = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN), switchMap(() => { return this.authService.hasValidAuthenticationToken().pipe( map((token: AuthTokenInfo) => new AuthenticatedAction(token)), - catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction())) + catchError((error: unknown) => observableOf(new CheckAuthenticationTokenCookieAction())), ); - }) + }), )); public checkTokenCookie$: Observable = createEffect(() => this.actions$.pipe( @@ -160,9 +195,9 @@ export class AuthEffects { return new RetrieveAuthMethodsAction(response); } }), - catchError((error) => observableOf(new AuthenticatedErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticatedErrorAction, error)), ); - }) + }), )); public retrieveToken$: Observable = createEffect(() => this.actions$.pipe( @@ -171,24 +206,24 @@ export class AuthEffects { return this.authService.refreshAuthenticationToken(null).pipe( take(1), map((token: AuthTokenInfo) => new AuthenticationSuccessAction(token)), - catchError((error) => observableOf(new AuthenticationErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticationErrorAction, error)), ); - }) + }), )); public refreshToken$: Observable = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN), switchMap((action: RefreshTokenAction) => { return this.authService.refreshAuthenticationToken(action.payload).pipe( map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)), - catchError((error) => observableOf(new RefreshTokenErrorAction())) + catchError((error: unknown) => observableOf(new RefreshTokenErrorAction())), ); - }) + }), )); // It means "reacts to this action but don't send another" public refreshTokenSuccess$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS), - tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)) + tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)), ), { dispatch: false }); /** @@ -204,7 +239,7 @@ export class AuthEffects { take(1), filter(([loaded, authenticated]) => loaded && !authenticated), tap(() => this.authService.removeToken()), - tap(() => this.authService.resetAuthenticationError()) + tap(() => this.authService.resetAuthenticationError()), ); })), { dispatch: false }); @@ -213,9 +248,9 @@ export class AuthEffects { * authorizations endpoint, to be sure to have consistent responses after a login with external idp * */ - invalidateAuthorizationsRequestCache$ = createEffect(() => this.actions$ + invalidateAuthorizationsRequestCache$ = createEffect(() => this.actions$ .pipe(ofType(StoreActionTypes.REHYDRATE), - tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache()) + tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache()), ), { dispatch: false }); public logOut$: Observable = createEffect(() => this.actions$ @@ -224,24 +259,24 @@ export class AuthEffects { switchMap(() => { this.authService.stopImpersonating(); return this.authService.logout().pipe( - map((value) => new LogOutSuccessAction()), - catchError((error) => observableOf(new LogOutErrorAction(error))) + map(() => new LogOutSuccessAction()), + catchError((error: unknown) => errorToAuthAction$(LogOutErrorAction, error)), ); - }) + }), )); public logOutSuccess$: Observable = createEffect(() => this.actions$ .pipe(ofType(AuthActionTypes.LOG_OUT_SUCCESS), tap(() => this.authService.removeToken()), tap(() => this.authService.clearRedirectUrl()), - tap(() => this.authService.refreshAfterLogout()) + tap(() => this.authService.refreshAfterLogout()), ), { dispatch: false }); public redirectToLoginTokenExpired$: Observable = createEffect(() => this.actions$ .pipe( ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED), tap(() => this.authService.removeToken()), - tap(() => this.authService.redirectToLoginWhenTokenExpired()) + tap(() => this.authService.redirectToLoginWhenTokenExpired()), ), { dispatch: false }); public retrieveMethods$: Observable = createEffect(() => this.actions$ @@ -251,9 +286,9 @@ export class AuthEffects { return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload) .pipe( map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)), - catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction())) + catchError(() => observableOf(new RetrieveAuthMethodsErrorAction())), ); - }) + }), )); /** @@ -268,7 +303,7 @@ export class AuthEffects { // in, and start a new timer switchMap(() => // Start a timer outside of Angular's zone - timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler)) + timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler)), ), // Re-enter the zone to dispatch the action observeOn(new EnterZoneScheduler(this.zone, queueScheduler)), diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 04bbc4acaf0..d824df472a2 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -1,18 +1,20 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; - import { Store } from '@ngrx/store'; import { of as observableOf } from 'rxjs'; -import { AuthInterceptor } from './auth.interceptor'; -import { AuthService } from './auth.service'; -import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; -import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RestRequestMethod } from '../data/rest-request-method'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { AuthInterceptor } from './auth.interceptor'; +import { AuthService } from './auth.service'; describe(`AuthInterceptor`, () => { let service: DspaceRestService; @@ -20,10 +22,8 @@ describe(`AuthInterceptor`, () => { const authServiceStub = new AuthServiceStub(); const store: Store = jasmine.createSpyObj('store', { - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - select: observableOf(true) + select: observableOf(true), }); beforeEach(() => { @@ -46,6 +46,10 @@ describe(`AuthInterceptor`, () => { httpMock = TestBed.inject(HttpTestingController); }); + afterEach(() => { + httpMock.verify(); + }); + describe('when has a valid token', () => { it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => { @@ -95,14 +99,11 @@ describe(`AuthInterceptor`, () => { }); it('should redirect to login', () => { - - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => { - expect(response).toBeTruthy(); - }); - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user'); httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems'); + // HttpTestingController.expectNone will throw an error when a requests is made + expect().nothing(); }); }); diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 4ad856fd882..84d4c532101 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -1,7 +1,3 @@ -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; - -import { catchError, map } from 'rxjs/operators'; -import { Injectable, Injector } from '@angular/core'; import { HttpErrorResponse, HttpEvent, @@ -10,19 +6,36 @@ import { HttpInterceptor, HttpRequest, HttpResponse, - HttpResponseBase + HttpResponseBase, } from '@angular/common/http'; +import { + Injectable, + Injector, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { + Observable, + of as observableOf, + throwError as observableThrowError, +} from 'rxjs'; +import { + catchError, + map, +} from 'rxjs/operators'; import { AppState } from '../../app.reducer'; -import { AuthService } from './auth.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util'; +import { + hasValue, + isNotEmpty, + isNotNull, +} from '../../shared/empty.util'; import { RedirectWhenTokenExpiredAction } from './auth.actions'; -import { Store } from '@ngrx/store'; -import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; @Injectable() export class AuthInterceptor implements HttpInterceptor { @@ -207,8 +220,8 @@ export class AuthInterceptor implements HttpInterceptor { message: 'Unknown auth error', status: 500, timestamp: Date.now(), - path: '' - }; + path: '', + }; } } else { authStatus.error = error; @@ -261,7 +274,7 @@ export class AuthInterceptor implements HttpInterceptor { // login successfully const newToken = response.headers.get('authorization'); authRes = response.clone({ - body: this.makeAuthStatusObject(true, newToken) + body: this.makeAuthStatusObject(true, newToken), }); // clean eventually refresh Requests list @@ -269,13 +282,13 @@ export class AuthInterceptor implements HttpInterceptor { } else if (this.isStatusResponse(response)) { authRes = response.clone({ body: Object.assign(response.body, { - authMethods: this.parseAuthMethodsFromHeaders(response.headers) - }) + authMethods: this.parseAuthMethodsFromHeaders(response.headers), + }), }); } else { // logout successfully authRes = response.clone({ - body: this.makeAuthStatusObject(false) + body: this.makeAuthStatusObject(false), }); } return authRes; @@ -283,7 +296,7 @@ export class AuthInterceptor implements HttpInterceptor { return response; } }), - catchError((error, caught) => { + catchError((error: unknown, caught) => { // Intercept an error response if (error instanceof HttpErrorResponse) { @@ -298,7 +311,7 @@ export class AuthInterceptor implements HttpInterceptor { headers: error.headers, status: error.status, statusText: error.statusText, - url: error.url + url: error.url, }); return observableOf(authResponse); } else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 41c03126538..7860744aa5b 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -1,4 +1,4 @@ -import { authReducer, AuthState } from './auth.reducer'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; import { AddAuthenticationMessageAction, AuthenticateAction, @@ -8,7 +8,6 @@ import { AuthenticationErrorAction, AuthenticationSuccessAction, CheckAuthenticationTokenAction, - SetAuthCookieStatus, CheckAuthenticationTokenCookieAction, LogOutAction, LogOutErrorAction, @@ -24,15 +23,19 @@ import { RetrieveAuthMethodsAction, RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, + SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, - UnsetUserAsIdleAction + UnsetUserAsIdleAction, } from './auth.actions'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AuthStatus } from './models/auth-status.model'; +import { + authReducer, + AuthState, +} from './auth.reducer'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; describe('authReducer', () => { @@ -47,7 +50,7 @@ describe('authReducer', () => { loaded: false, blocking: true, loading: false, - idle: false + idle: false, }; const action = new AuthenticateAction('user', 'password'); const newState = authReducer(initialState, action); @@ -58,7 +61,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); @@ -72,7 +75,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticationSuccessAction(mockTokenInfo); const newState = authReducer(initialState, action); @@ -88,7 +91,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticationErrorAction(mockError); const newState = authReducer(initialState, action); @@ -100,7 +103,7 @@ describe('authReducer', () => { info: undefined, authToken: undefined, error: 'Test error message', - idle: false + idle: false, }; expect(newState).toEqual(state); @@ -114,7 +117,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedAction(mockTokenInfo); const newState = authReducer(initialState, action); @@ -125,7 +128,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -138,7 +141,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href); const newState = authReducer(initialState, action); @@ -150,7 +153,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -163,7 +166,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedErrorAction(mockError); const newState = authReducer(initialState, action); @@ -175,7 +178,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -186,7 +189,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new CheckAuthenticationTokenAction(); const newState = authReducer(initialState, action); @@ -195,7 +198,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -206,7 +209,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; const action = new CheckAuthenticationTokenCookieAction(); const newState = authReducer(initialState, action); @@ -215,7 +218,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -227,7 +230,7 @@ describe('authReducer', () => { blocking: false, loading: true, externalAuth: false, - idle: false + idle: false, }; const action = new SetAuthCookieStatus(true); const newState = authReducer(initialState, action); @@ -237,7 +240,7 @@ describe('authReducer', () => { blocking: false, loading: true, externalAuth: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -252,7 +255,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutAction(); @@ -271,7 +274,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutSuccessAction(); @@ -286,7 +289,7 @@ describe('authReducer', () => { info: undefined, refreshing: false, userId: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -301,7 +304,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutErrorAction(mockError); @@ -315,7 +318,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -329,7 +332,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id); const newState = authReducer(initialState, action); @@ -342,7 +345,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -355,7 +358,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new RetrieveAuthenticatedEpersonErrorAction(mockError); const newState = authReducer(initialState, action); @@ -367,7 +370,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -382,7 +385,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenAction(newTokenInfo); @@ -397,7 +400,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -413,7 +416,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenSuccessAction(newTokenInfo); @@ -428,7 +431,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: false, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -444,7 +447,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; const action = new RefreshTokenErrorAction(); const newState = authReducer(initialState, action); @@ -458,7 +461,7 @@ describe('authReducer', () => { info: undefined, refreshing: false, userId: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -473,7 +476,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; state = { @@ -485,7 +488,7 @@ describe('authReducer', () => { error: undefined, info: 'Message', userId: undefined, - idle: false + idle: false, }; }); @@ -507,7 +510,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new AddAuthenticationMessageAction('Message'); const newState = authReducer(initialState, action); @@ -517,7 +520,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: 'Message', - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -530,7 +533,7 @@ describe('authReducer', () => { loading: false, error: 'Error', info: 'Message', - idle: false + idle: false, }; const action = new ResetAuthenticationMessagesAction(); const newState = authReducer(initialState, action); @@ -541,7 +544,7 @@ describe('authReducer', () => { loading: false, error: undefined, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -552,7 +555,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new SetRedirectUrlAction('redirect.url'); const newState = authReducer(initialState, action); @@ -562,7 +565,7 @@ describe('authReducer', () => { blocking: false, loading: false, redirectUrl: 'redirect.url', - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -574,7 +577,7 @@ describe('authReducer', () => { blocking: false, loading: false, authMethods: [], - idle: false + idle: false, }; const action = new RetrieveAuthMethodsAction(new AuthStatus()); const newState = authReducer(initialState, action); @@ -584,7 +587,7 @@ describe('authReducer', () => { blocking: false, loading: true, authMethods: [], - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -596,7 +599,7 @@ describe('authReducer', () => { blocking: true, loading: true, authMethods: [], - idle: false + idle: false, }; const authMethods: AuthMethod[] = [ new AuthMethod(AuthMethodType.Password, 0), @@ -610,7 +613,7 @@ describe('authReducer', () => { blocking: false, loading: false, authMethods: authMethods, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -622,7 +625,7 @@ describe('authReducer', () => { blocking: true, loading: true, authMethods: [], - idle: false + idle: false, }; const action = new RetrieveAuthMethodsErrorAction(); @@ -633,7 +636,7 @@ describe('authReducer', () => { blocking: false, loading: false, authMethods: [new AuthMethod(AuthMethodType.Password, 0)], - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -644,7 +647,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: false + idle: false, }; const action = new SetUserAsIdleAction(); @@ -654,7 +657,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: true + idle: true, }; expect(newState).toEqual(state); }); @@ -665,7 +668,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: true + idle: true, }; const action = new UnsetUserAsIdleAction(); @@ -675,7 +678,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: false + idle: false, }; expect(newState).toEqual(state); }); diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 437c19fd26e..8a399710eae 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -1,4 +1,5 @@ // import actions +import { StoreActionTypes } from '../../store.actions'; import { AddAuthenticationMessageAction, AuthActions, @@ -10,14 +11,14 @@ import { RedirectWhenTokenExpiredAction, RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction, - RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus, - SetRedirectUrlAction + RetrieveAuthMethodsSuccessAction, + SetAuthCookieStatus, + SetRedirectUrlAction, } from './auth.actions'; -// import models -import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; -import { StoreActionTypes } from '../../store.actions'; +// import models +import { AuthTokenInfo } from './models/auth-token-info.model'; /** * The auth state. @@ -76,7 +77,7 @@ const initialState: AuthState = { loading: false, authMethods: [], externalAuth: false, - idle: false + idle: false, }; /** @@ -92,13 +93,13 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { error: undefined, loading: true, - info: undefined + info: undefined, }); case AuthActionTypes.AUTHENTICATED: return Object.assign({}, state, { loading: true, - blocking: true + blocking: true, }); case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: @@ -109,7 +110,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.SET_AUTH_COOKIE_STATUS: return Object.assign({}, state, { - externalAuth: (action as SetAuthCookieStatus).payload + externalAuth: (action as SetAuthCookieStatus).payload, }); case AuthActionTypes.AUTHENTICATED_ERROR: @@ -120,13 +121,13 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut error: (action as AuthenticationErrorAction).payload.message, loaded: true, blocking: false, - loading: false + loading: false, }); case AuthActionTypes.AUTHENTICATED_SUCCESS: return Object.assign({}, state, { authenticated: true, - authToken: (action as AuthenticatedSuccessAction).payload.authToken + authToken: (action as AuthenticatedSuccessAction).payload.authToken, }); case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: @@ -136,7 +137,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, blocking: false, info: undefined, - userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload + userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload, }); case AuthActionTypes.AUTHENTICATE_ERROR: @@ -145,7 +146,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authToken: undefined, error: (action as AuthenticationErrorAction).payload.message, blocking: false, - loading: false + loading: false, }); case AuthActionTypes.AUTHENTICATE_SUCCESS: @@ -155,7 +156,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.LOG_OUT_ERROR: return Object.assign({}, state, { authenticated: true, - error: (action as LogOutErrorAction).payload.message + error: (action as LogOutErrorAction).payload.message, }); case AuthActionTypes.REFRESH_TOKEN_ERROR: @@ -168,7 +169,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, info: undefined, refreshing: false, - userId: undefined + userId: undefined, }); case AuthActionTypes.LOG_OUT_SUCCESS: @@ -181,7 +182,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: true, info: undefined, refreshing: false, - userId: undefined + userId: undefined, }); case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED: @@ -193,7 +194,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut blocking: false, loading: false, info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload, - userId: undefined + userId: undefined, }); case AuthActionTypes.REFRESH_TOKEN: @@ -205,7 +206,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { authToken: (action as RefreshTokenSuccessAction).payload, refreshing: false, - blocking: false + blocking: false, }); case AuthActionTypes.ADD_MESSAGE: @@ -229,14 +230,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { loading: false, blocking: false, - authMethods: (action as RetrieveAuthMethodsSuccessAction).payload + authMethods: (action as RetrieveAuthMethodsSuccessAction).payload, }); case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR: return Object.assign({}, state, { loading: false, blocking: false, - authMethods: [new AuthMethod(AuthMethodType.Password, 0)] + authMethods: [new AuthMethod(AuthMethodType.Password, 0)], }); case AuthActionTypes.SET_REDIRECT_URL: diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index b38d17aecdb..66c80b9bf5a 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -1,46 +1,75 @@ -import { inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Store, StoreModule } from '@ngrx/store'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; -import { Observable, of as observableOf } from 'rxjs'; -import { authReducer, AuthState } from './auth.reducer'; -import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { AuthService, IMPERSONATING_COOKIE } from './auth.service'; -import { RouterStub } from '../../shared/testing/router.stub'; +import { + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { REQUEST } from '../../../express.tokens'; +import { AppState } from '../../app.reducer'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { CookieService } from '../services/cookie.service'; import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub'; -import { AuthRequestService } from './auth-request.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EPerson } from '../eperson/models/eperson.model'; +import { authMethodsMock } from '../../shared/testing/auth-service.stub'; import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AppState } from '../../app.reducer'; -import { ClientCookieService } from '../services/client-cookie.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; -import { RouteService } from '../services/route.service'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { + SpecialGroupDataMock, + SpecialGroupDataMock$, +} from '../../shared/testing/special-group.mock'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { authMethodsMock } from '../../shared/testing/auth-service.stub'; -import { AuthMethod } from './models/auth.method'; +import { EPerson } from '../eperson/models/eperson.model'; +import { ClientCookieService } from '../services/client-cookie.service'; +import { CookieService } from '../services/cookie.service'; import { HardRedirectService } from '../services/hard-redirect.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions'; -import { SpecialGroupDataMock, SpecialGroupDataMock$ } from '../../shared/testing/special-group.mock'; -import { cold } from 'jasmine-marbles'; +import { RouteService } from '../services/route.service'; +import { + NativeWindowRef, + NativeWindowService, +} from '../services/window.service'; +import { + SetUserAsIdleAction, + UnsetUserAsIdleAction, +} from './auth.actions'; +import { + authReducer, + AuthState, +} from './auth.reducer'; +import { + AuthService, + IMPERSONATING_COOKIE, +} from './auth.service'; +import { AuthRequestService } from './auth-request.service'; +import { AuthMethod } from './models/auth.method'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; describe('AuthService test', () => { const mockEpersonDataService: any = { findByHref(href: string): Observable> { return createSuccessfulRemoteDataObject$(EPersonMock); - } + }, }; let mockStore: Store; @@ -62,13 +91,13 @@ describe('AuthService test', () => { uuid: 'test', authenticated: true, okay: true, - specialGroups: SpecialGroupDataMock$ + specialGroups: SpecialGroupDataMock$, }); function init() { mockStore = jasmine.createSpyObj('store', { dispatch: {}, - pipe: observableOf(true) + pipe: observableOf(true), }); window = new NativeWindowRef(); routerStub = new RouterStub(); @@ -80,7 +109,7 @@ describe('AuthService test', () => { loading: false, authToken: token, user: EPersonMock, - idle: false + idle: false, }; unAuthenticatedState = { authenticated: false, @@ -88,7 +117,7 @@ describe('AuthService test', () => { loading: false, authToken: undefined, user: undefined, - idle: false + idle: false, }; idleState = { authenticated: true, @@ -96,12 +125,12 @@ describe('AuthService test', () => { loading: false, authToken: token, user: EPersonMock, - idle: true + idle: true, }; authRequest = new AuthRequestServiceStub(); routeStub = new ActivatedRouteStub(); linkService = { - resolveLinks: {} + resolveLinks: {}, }; hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']); spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) }); @@ -117,11 +146,10 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } + strictActionImmutability: false, + }, }), ], - declarations: [], providers: [ { provide: AuthRequestService, useValue: authRequest }, { provide: NativeWindowService, useValue: window }, @@ -135,7 +163,7 @@ describe('AuthService test', () => { { provide: NotificationsService, useValue: NotificationsServiceStub }, { provide: TranslateService, useValue: getMockTranslateService() }, CookieService, - AuthService + AuthService, ], }); authService = TestBed.inject(AuthService); @@ -238,9 +266,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -249,8 +277,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -313,9 +341,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -325,8 +353,8 @@ describe('AuthService test', () => { { provide: RemoteDataBuildService, useValue: linkService }, ClientCookieService, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -338,7 +366,7 @@ describe('AuthService test', () => { loaded: true, loading: false, authToken: expiredToken, - user: EPersonMock + user: EPersonMock, }; store .subscribe((state) => { @@ -528,7 +556,7 @@ describe('AuthService test', () => { it('should call navigateToRedirectUrl with no url', () => { const expectRes = cold('(a|)', { - a: SpecialGroupDataMock + a: SpecialGroupDataMock, }); expect(authService.getSpecialGroupsFromAuthStatus()).toBeObservable(expectRes); }); @@ -543,9 +571,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -554,8 +582,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -583,9 +611,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -594,8 +622,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 8b08b4f32db..cd773b68cfa 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,18 +1,34 @@ -import { Inject, Injectable, Optional } from '@angular/core'; -import { Router } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; -import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; - -import { Observable, of as observableOf } from 'rxjs'; -import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; -import { select, Store } from '@ngrx/store'; +import { + Inject, + Injectable, + Optional, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { + select, + Store, +} from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; import { CookieAttributes } from 'js-cookie'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + filter, + map, + startWith, + switchMap, + take, +} from 'rxjs/operators'; -import { EPerson } from '../eperson/models/eperson.model'; -import { AuthRequestService } from './auth-request.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; +import { environment } from '../../../environments/environment'; +import { + REQUEST, + RESPONSE, +} from '../../../express.tokens'; +import { AppState } from '../../app.reducer'; import { hasNoValue, hasValue, @@ -20,42 +36,58 @@ import { isEmpty, isNotEmpty, isNotNull, - isNotUndefined + isNotUndefined, } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { EPersonDataService } from '../eperson/eperson-data.service'; +import { EPerson } from '../eperson/models/eperson.model'; +import { Group } from '../eperson/models/group.model'; import { CookieService } from '../services/cookie.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { RouteService } from '../services/route.service'; import { - getAuthenticatedUserId, - getAuthenticationToken, getExternalAuthCookieStatus, - getRedirectUrl, - isAuthenticated, - isAuthenticatedLoaded, - isIdle, - isTokenRefreshing -} from './selectors'; -import { AppState } from '../../app.reducer'; + NativeWindowRef, + NativeWindowService, +} from '../services/window.service'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, +} from '../shared/operators'; +import { PageInfo } from '../shared/page-info.model'; import { CheckAuthenticationTokenAction, RefreshTokenAction, - ResetAuthenticationMessagesAction, SetAuthCookieStatus, + ResetAuthenticationMessagesAction, + SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, - UnsetUserAsIdleAction + UnsetUserAsIdleAction, } from './auth.actions'; -import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { RouteService } from '../services/route.service'; -import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators'; +import { AuthRequestService } from './auth-request.service'; import { AuthMethod } from './models/auth.method'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { RemoteData } from '../data/remote-data'; -import { environment } from '../../../environments/environment'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model'; -import { Group } from '../eperson/models/group.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { PageInfo } from '../shared/page-info.model'; -import { followLink } from '../../shared/utils/follow-link-config.model'; +import { AuthStatus } from './models/auth-status.model'; +import { + AuthTokenInfo, + TOKENITEM, +} from './models/auth-token-info.model'; +import { + getAuthenticatedUserId, + getAuthenticationToken, + getExternalAuthCookieStatus, + getRedirectUrl, + isAuthenticated, + isAuthenticatedLoaded, + isIdle, + isTokenRefreshing, +} from './selectors'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -65,7 +97,7 @@ export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson'; /** * The auth service. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class AuthService { /** @@ -90,13 +122,13 @@ export class AuthService { protected store: Store, protected hardRedirectService: HardRedirectService, private notificationService: NotificationsService, - private translateService: TranslateService + private translateService: TranslateService, ) { this.store.pipe( // when this service is constructed the store is not fully initialized yet filter((state: any) => state?.core?.auth !== undefined), select(isAuthenticated), - startWith(false) + startWith(false), ).subscribe((authenticated: boolean) => this._authenticated = authenticated); } @@ -136,7 +168,7 @@ export class AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)), ); } @@ -202,7 +234,7 @@ export class AuthService { */ public retrieveAuthenticatedUserByHref(userHref: string): Observable { return this.epersonService.findByHref(userHref).pipe( - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -212,7 +244,7 @@ export class AuthService { */ public retrieveAuthenticatedUserById(userId: string): Observable { return this.epersonService.findById(userId).pipe( - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -225,7 +257,7 @@ export class AuthService { select(getAuthenticatedUserId), hasValueOperator(), switchMap((id: string) => this.epersonService.findById(id)), - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -248,7 +280,7 @@ export class AuthService { } else { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(),[])); } - }) + }), ); } @@ -260,15 +292,14 @@ export class AuthService { select(getAuthenticationToken), take(1), map((authTokenInfo: AuthTokenInfo) => { - let token: AuthTokenInfo; // Retrieve authentication token info and check if is valid - token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM); + const token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM); if (isNotEmpty(token) && token.hasOwnProperty('accessToken') && isNotEmpty(token.accessToken) && !this.isTokenExpired(token)) { return token; } else { throw false; } - }) + }), ); } @@ -412,7 +443,7 @@ export class AuthService { const token = this.getToken(); return token.expires - (60 * 5 * 1000) < Date.now(); } - }) + }), ); } @@ -437,7 +468,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; // Save cookie with the token return this.storage.set(TOKENITEM, token, options); @@ -516,7 +547,7 @@ export class AuthService { } else { return this.storage.get(REDIRECT_COOKIE); } - }) + }), ); } @@ -529,7 +560,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; this.storage.set(REDIRECT_COOKIE, url, options); this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); } @@ -609,7 +640,7 @@ export class AuthService { */ getShortlivedToken(): Observable { return this.isAuthenticated().pipe( - switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)) + switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)), ); } diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts index 1ab1d2e0a51..eba6dc89f9e 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -1,65 +1,64 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateChildFn, + CanActivateFn, Router, RouterStateSnapshot, - UrlTree + UrlTree, } from '@angular/router'; - +import { + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { map, find, switchMap } from 'rxjs/operators'; -import { select, Store } from '@ngrx/store'; +import { + find, + map, + switchMap, +} from 'rxjs/operators'; -import { isAuthenticated, isAuthenticationLoading } from './selectors'; -import { AuthService, LOGIN_ROUTE } from './auth.service'; -import { CoreState } from '../core-state.model'; +import { AppState } from '../../app.reducer'; +import { + AuthService, + LOGIN_ROUTE, +} from './auth.service'; +import { + isAuthenticated, + isAuthenticationLoading, +} from './selectors'; /** * Prevent unauthorized activating and loading of routes - * @class AuthenticatedGuard + * True when user is authenticated + * UrlTree with redirect to login page when user isn't authenticated + * @method canActivate */ -@Injectable() -export class AuthenticatedGuard implements CanActivate { - - /** - * @constructor - */ - constructor(private authService: AuthService, private router: Router, private store: Store) {} - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const url = state.url; - return this.handleAuth(url); - } - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivateChild - */ - canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.canActivate(route, state); - } +export const authenticatedGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authService: AuthService = inject(AuthService), + router: Router = inject(Router), + store: Store = inject(Store), +): Observable => { + const url = state.url; + // redirect to sign in page if user is not authenticated + return store.pipe(select(isAuthenticationLoading)).pipe( + find((isLoading: boolean) => isLoading === false), + switchMap(() => store.pipe(select(isAuthenticated))), + map((authenticated) => { + if (authenticated) { + return authenticated; + } else { + authService.setRedirectUrl(url); + authService.removeToken(); + return router.createUrlTree([LOGIN_ROUTE]); + } + }), + ); +}; - private handleAuth(url: string): Observable { - // redirect to sign in page if user is not authenticated - return this.store.pipe(select(isAuthenticationLoading)).pipe( - find((isLoading: boolean) => isLoading === false), - switchMap(() => this.store.pipe(select(isAuthenticated))), - map((authenticated) => { - if (authenticated) { - return authenticated; - } else { - this.authService.setRedirectUrl(url); - this.authService.removeToken(); - return this.router.createUrlTree([LOGIN_ROUTE]); - } - }) - ); - } -} +export const AuthenticatedGuardChild: CanActivateChildFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => authenticatedGuard(route, state); diff --git a/src/app/core/auth/browser-auth-request.service.spec.ts b/src/app/core/auth/browser-auth-request.service.spec.ts index b41d981bcf6..9649255b236 100644 --- a/src/app/core/auth/browser-auth-request.service.spec.ts +++ b/src/app/core/auth/browser-auth-request.service.spec.ts @@ -1,8 +1,9 @@ -import { AuthRequestService } from './auth-request.service'; -import { RequestService } from '../data/request.service'; -import { BrowserAuthRequestService } from './browser-auth-request.service'; import { Observable } from 'rxjs'; + import { PostRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { AuthRequestService } from './auth-request.service'; +import { BrowserAuthRequestService } from './browser-auth-request.service'; describe(`BrowserAuthRequestService`, () => { let href: string; @@ -12,7 +13,7 @@ describe(`BrowserAuthRequestService`, () => { beforeEach(() => { href = 'https://rest.api/auth/shortlivedtokens'; requestService = jasmine.createSpyObj('requestService', { - 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' + 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2', }); service = new BrowserAuthRequestService(null, requestService, null); }); diff --git a/src/app/core/auth/browser-auth-request.service.ts b/src/app/core/auth/browser-auth-request.service.ts index 485e2ef9c4a..d708cd8982a 100644 --- a/src/app/core/auth/browser-auth-request.service.ts +++ b/src/app/core/auth/browser-auth-request.service.ts @@ -1,10 +1,14 @@ import { Injectable } from '@angular/core'; -import { AuthRequestService } from './auth-request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { PostRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthRequestService } from './auth-request.service'; /** * Client side version of the service to send authentication requests @@ -15,7 +19,7 @@ export class BrowserAuthRequestService extends AuthRequestService { constructor( halService: HALEndpointService, requestService: RequestService, - rdbService: RemoteDataBuildService + rdbService: RemoteDataBuildService, ) { super(halService, requestService, rdbService); } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index d18b1ccf9a6..1a6938887d5 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,7 +1,17 @@ -import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { + autoserialize, + deserialize, + deserializeAs, +} from 'cerialize'; import { Observable } from 'rxjs'; -import { link, typedObject } from '../../cache/builders/build-decorators'; + +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; import { EPerson } from '../../eperson/models/eperson.model'; import { EPERSON } from '../../eperson/models/eperson.resource-type'; @@ -10,12 +20,10 @@ import { GROUP } from '../../eperson/models/group.resource-type'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { AuthMethod } from './auth.method'; import { AuthError } from './auth-error.model'; import { AUTH_STATUS } from './auth-status.resource-type'; import { AuthTokenInfo } from './auth-token-info.model'; -import { AuthMethod } from './auth.method'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { PaginatedList } from '../../data/paginated-list.model'; /** * Object that represents the authenticated status of a user @@ -28,14 +36,14 @@ export class AuthStatus implements CacheableObject { * The unique identifier of this auth status */ @autoserialize - id: string; + id: string; /** * The type for this AuthStatus */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The UUID of this auth status @@ -43,25 +51,25 @@ export class AuthStatus implements CacheableObject { * It is based on the ID, so it will be the same for each refresh. */ @deserializeAs(new IDToUUIDSerializer('auth-status'), 'id') - uuid: string; + uuid: string; /** * True if REST API is up and running, should never return false */ @autoserialize - okay: boolean; + okay: boolean; /** * If the auth status represents an authenticated state */ @autoserialize - authenticated: boolean; + authenticated: boolean; /** * The {@link HALLink}s for this AuthStatus */ @deserialize - _links: { + _links: { self: HALLink; eperson: HALLink; specialGroups: HALLink; @@ -72,32 +80,32 @@ export class AuthStatus implements CacheableObject { * Will be undefined unless the eperson {@link HALLink} has been resolved. */ @link(EPERSON) - eperson?: Observable>; + eperson?: Observable>; /** * The SpecialGroup of this auth status * Will be undefined unless the SpecialGroup {@link HALLink} has been resolved. */ @link(GROUP, true) - specialGroups?: Observable>>; + specialGroups?: Observable>>; /** * True if the token is valid, false if there was no token or the token wasn't valid */ @autoserialize - token?: AuthTokenInfo; + token?: AuthTokenInfo; /** * Authentication error if there was one for this status */ // TODO should be refactored to use the RemoteData error @autoserialize - error?: AuthError; + error?: AuthError; /** * All authentication methods enabled at the backend */ @autoserialize - authMethods: AuthMethod[]; + authMethods: AuthMethod[]; } diff --git a/src/app/core/auth/models/short-lived-token.model.ts b/src/app/core/auth/models/short-lived-token.model.ts index 3786bd8e6a0..d91a26e990a 100644 --- a/src/app/core/auth/models/short-lived-token.model.ts +++ b/src/app/core/auth/models/short-lived-token.model.ts @@ -1,10 +1,15 @@ +import { + autoserialize, + autoserializeAs, + deserialize, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; import { SHORT_LIVED_TOKEN } from './short-lived-token.resource-type'; -import { HALLink } from '../../shared/hal-link.model'; -import { CacheableObject } from '../../cache/cacheable-object.model'; /** * A short-lived token that can be used to authenticate a rest request @@ -17,19 +22,19 @@ export class ShortLivedToken implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The value for this ShortLivedToken */ @autoserializeAs('token') - value: string; + value: string; /** * The {@link HALLink}s for this ShortLivedToken */ @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index aba739edf67..63603776263 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -1,5 +1,7 @@ import { createSelector } from '@ngrx/store'; +import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; /** * Every reducer module's default export is the reducer function itself. In * addition, each module should export a type or interface that describes @@ -7,8 +9,6 @@ import { createSelector } from '@ngrx/store'; * notation packages up all of the exports into a single object. */ import { AuthState } from './auth.reducer'; -import { CoreState } from '../core-state.model'; -import { coreSelector } from '../core.selectors'; /** * Returns the user state. diff --git a/src/app/core/auth/server-auth-request.service.spec.ts b/src/app/core/auth/server-auth-request.service.spec.ts index 5b0221e5df4..b119f6b3b76 100644 --- a/src/app/core/auth/server-auth-request.service.spec.ts +++ b/src/app/core/auth/server-auth-request.service.spec.ts @@ -1,14 +1,22 @@ -import { AuthRequestService } from './auth-request.service'; +import { + HttpClient, + HttpHeaders, + HttpResponse, +} from '@angular/common/http'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { PostRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { ServerAuthRequestService } from './server-auth-request.service'; -import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; -import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { PostRequest } from '../data/request.models'; import { XSRF_REQUEST_HEADER, - XSRF_RESPONSE_HEADER + XSRF_RESPONSE_HEADER, } from '../xsrf/xsrf.constants'; +import { AuthRequestService } from './auth-request.service'; +import { ServerAuthRequestService } from './server-auth-request.service'; describe(`ServerAuthRequestService`, () => { let href: string; @@ -22,20 +30,20 @@ describe(`ServerAuthRequestService`, () => { beforeEach(() => { href = 'https://rest.api/auth/shortlivedtokens'; requestService = jasmine.createSpyObj('requestService', { - 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' + 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2', }); let headers = new HttpHeaders(); headers = headers.set(XSRF_RESPONSE_HEADER, mockToken); httpResponse = { body: { bar: false }, headers: headers, - statusText: '200' + statusText: '200', } as HttpResponse; httpClient = jasmine.createSpyObj('httpClient', { get: observableOf(httpResponse), }); halService = jasmine.createSpyObj('halService', { - 'getRootHref': '/api' + 'getRootHref': '/api', }); service = new ServerAuthRequestService(halService, requestService, null, httpClient); }); diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 058322acce0..5f1828c71c5 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -1,21 +1,22 @@ -import { Injectable } from '@angular/core'; -import { AuthRequestService } from './auth-request.service'; -import { PostRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { - HttpHeaders, HttpClient, - HttpResponse + HttpHeaders, + HttpResponse, } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { PostRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { + DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER, - DSPACE_XSRF_COOKIE } from '../xsrf/xsrf.constants'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { AuthRequestService } from './auth-request.service'; /** * Server side version of the service to send authentication requests @@ -45,11 +46,11 @@ export class ServerAuthRequestService extends AuthRequestService { map((response: HttpResponse) => response.headers.get(XSRF_RESPONSE_HEADER)), // Use that token to create an HttpHeaders object map((xsrfToken: string) => new HttpHeaders() - .set('Content-Type', 'application/json; charset=utf-8') - // set the token as the XSRF header - .set(XSRF_REQUEST_HEADER, xsrfToken) - // and as the DSPACE-XSRF-COOKIE - .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), + .set('Content-Type', 'application/json; charset=utf-8') + // set the token as the XSRF header + .set(XSRF_REQUEST_HEADER, xsrfToken) + // and as the DSPACE-XSRF-COOKIE + .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), map((headers: HttpHeaders) => // Create a new PostRequest using those headers and the given href new PostRequest( @@ -59,8 +60,8 @@ export class ServerAuthRequestService extends AuthRequestService { { headers: headers, }, - ) - ) + ), + ), ); } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index fc8ab18bfb6..f51215abada 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,15 +1,17 @@ -import { Injectable } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; - +import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { RemoteData } from '../data/remote-data'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { RemoteData } from '../data/remote-data'; /** * The auth service. @@ -57,7 +59,7 @@ export class ServerAuthService extends AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)), ); } } diff --git a/src/app/core/auth/token-response-parsing.service.spec.ts b/src/app/core/auth/token-response-parsing.service.spec.ts index a440325560a..f4244e98b2c 100644 --- a/src/app/core/auth/token-response-parsing.service.spec.ts +++ b/src/app/core/auth/token-response-parsing.service.spec.ts @@ -1,6 +1,6 @@ -import { TokenResponseParsingService } from './token-response-parsing.service'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { TokenResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { TokenResponseParsingService } from './token-response-parsing.service'; describe('TokenResponseParsingService', () => { let service: TokenResponseParsingService; @@ -13,10 +13,10 @@ describe('TokenResponseParsingService', () => { it('should return a TokenResponse containing the token', () => { const data = { payload: { - token: 'valid-token' + token: 'valid-token', }, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; const expected = new TokenResponse(data.payload.token, true, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); @@ -26,7 +26,7 @@ describe('TokenResponseParsingService', () => { const data = { payload: {}, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; const expected = new TokenResponse(null, false, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); @@ -36,7 +36,7 @@ describe('TokenResponseParsingService', () => { const data = { payload: {}, statusCode: 400, - statusText: 'BAD REQUEST' + statusText: 'BAD REQUEST', } as RawRestResponse; const expected = new TokenResponse(null, false, 400, 'BAD REQUEST'); expect(service.parse(undefined, data)).toEqual(expected); diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts index 1ba7a16b14e..03a45521e93 100644 --- a/src/app/core/auth/token-response-parsing.service.ts +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -1,11 +1,15 @@ -import { ResponseParsingService } from '../data/parsing.service'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { RestResponse, TokenResponse } from '../cache/response.models'; -import { isNotEmpty } from '../../shared/empty.util'; import { Injectable } from '@angular/core'; + +import { isNotEmpty } from '../../shared/empty.util'; +import { + RestResponse, + TokenResponse, +} from '../cache/response.models'; +import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/rest-request.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) /** * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a token string * wrapped in a TokenResponse diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index b2ddade682c..5628fe65837 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,31 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Bitstream } from '../shared/bitstream.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root' -}) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor( - protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } +export const bitstreamBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService), + dataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; -} diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts index 333886ed3d1..11a3a743505 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts @@ -1,35 +1,46 @@ import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; -import { Observable, of as observableOf } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; - +import { getDSORoute } from '../../app-routing-paths'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { DSONameService } from './dso-name.service'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; import { LinkService } from '../cache/builders/link.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { RemoteData } from '../data/remote-data'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getDSORoute } from '../../app-routing-paths'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { BitstreamDataService } from '../data/bitstream-data.service'; -import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { RemoteData } from '../data/remote-data'; import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { DSONameService } from './dso-name.service'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { constructor( protected bitstreamService: BitstreamDataService, protected linkService: LinkService, - protected dsoNameService: DSONameService + protected dsoNameService: DSONameService, ) { super(linkService, dsoNameService); } @@ -53,7 +64,7 @@ export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { return observableOf([]); }), - map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]) + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]), ); } @@ -74,12 +85,12 @@ export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { } else { return observableOf(undefined); } - }) + }), ); } else { return observableOf(undefined); } - }) + }), ); } } diff --git a/src/app/core/breadcrumbs/breadcrumbsProviderService.ts b/src/app/core/breadcrumbs/breadcrumbsProviderService.ts index 4f5dd0a5838..83d9a2caaa9 100644 --- a/src/app/core/breadcrumbs/breadcrumbsProviderService.ts +++ b/src/app/core/breadcrumbs/breadcrumbsProviderService.ts @@ -1,6 +1,7 @@ -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { Observable } from 'rxjs'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; + /** * Service to calculate breadcrumbs for a single part of the route */ diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 46c49add064..7df656a9610 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,28 +1,36 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { Collection } from '../shared/collection.model'; -import { CollectionDataService } from '../data/collection-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { CollectionDataService } from '../data/collection-data.service'; +import { Collection } from '../shared/collection.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Collection + * The resolve function that resolves the BreadcrumbConfig object for a Collection */ -@Injectable({ - providedIn: 'root' -}) -export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { - super(breadcrumbService, dataService); - } +export const collectionBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CollectionDataService = inject(CollectionDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COLLECTION_PAGE_LINKS_TO_FOLLOW; - } -} diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 309927771d5..1064a1cc191 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -1,28 +1,35 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Community + * The resolve function that resolves the BreadcrumbConfig object for a Community */ -@Injectable({ - providedIn: 'root' -}) -export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COMMUNITY_PAGE_LINKS_TO_FOLLOW; - } -} +export const communityBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CommunityDataService = inject(CommunityDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index e35e26e46f9..ae19128d4e9 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -1,12 +1,12 @@ -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { Collection } from '../shared/collection.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getTestScheduler } from 'jasmine-marbles'; -import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; + +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Collection } from '../shared/collection.model'; +import { collectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; describe('DSOBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: DSOBreadcrumbResolver; + let resolver: any; let collectionService: any; let dsoBreadcrumbService: any; let testCollection: Collection; @@ -16,18 +16,18 @@ describe('DSOBreadcrumbResolver', () => { beforeEach(() => { uuid = '1234-65487-12354-1235'; - breadcrumbUrl = '/collections/' + uuid; - currentUrl = breadcrumbUrl + '/edit'; + breadcrumbUrl = `/collections/${uuid}`; + currentUrl = `${breadcrumbUrl}/edit`; testCollection = Object.assign(new Collection(), { uuid }); dsoBreadcrumbService = {}; collectionService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection) + findById: () => createSuccessfulRemoteDataObject$(testCollection), }; - resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService); + resolver = collectionBreadcrumbResolver; }); it('should resolve a breadcrumb config for the correct DSO', () => { - const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any); + const resolvedConfig = resolver({ params: { id: uuid } } as any, { url: currentUrl } as any, dsoBreadcrumbService, collectionService); const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl }; getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); }); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 8be4e5e099a..cb1f96b1033 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,56 +1,49 @@ -import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { map } from 'rxjs/operators'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { map } from 'rxjs/operators'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { hasValue } from '../../shared/empty.util'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export abstract class DSOBreadcrumbResolver implements Resolve> { - protected constructor( - protected breadcrumbService: DSOBreadcrumbsService, - protected dataService: IdentifiableDataService, - ) { - } - - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.params.id; - return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - map((object: T) => { - if (hasValue(object)) { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return { provider: this.breadcrumbService, key: object, url: url }; - } else { - return undefined; - } - }) - ); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - abstract get followLinks(): FollowLinkConfig[]; -} +export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { + const uuid = route.params.id; + return dataService.findById(uuid, true, false, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((object: DSpaceObject) => { + if (hasValue(object)) { + const fullPath = state.url; + const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid); + return { provider: breadcrumbService, key: object, url: url }; + } else { + return undefined; + } + }), + ); +}; diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts index 6b89c576d66..3869defa70d 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts @@ -1,17 +1,24 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + +import { getDSORoute } from '../../app-routing-paths'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { getMockLinkService } from '../../shared/mocks/link-service.mock'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { LinkService } from '../cache/builders/link.service'; -import { Item } from '../shared/item.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { of as observableOf } from 'rxjs'; -import { Community } from '../shared/community.model'; import { Collection } from '../shared/collection.model'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { getTestScheduler } from 'jasmine-marbles'; +import { Community } from '../shared/community.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Item } from '../shared/item.model'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSONameService } from './dso-name.service'; -import { getDSORoute } from '../../app-routing-paths'; describe('DSOBreadcrumbsService', () => { let service: DSOBreadcrumbsService; @@ -43,46 +50,46 @@ describe('DSOBreadcrumbsService', () => { { type: 'community', metadata: { - 'dc.title': [{ value: 'community' }] + 'dc.title': [{ value: 'community' }], }, uuid: communityUUID, parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), _links: { parentCommunity: 'site', - self: communityPath + communityUUID - } - } + self: communityPath + communityUUID, + }, + }, ); testCollection = Object.assign(new Collection(), { type: 'collection', metadata: { - 'dc.title': [{ value: 'collection' }] + 'dc.title': [{ value: 'collection' }], }, uuid: collectionUUID, parentCommunity: createSuccessfulRemoteDataObject$(testCommunity), _links: { parentCommunity: communityPath + communityUUID, - self: communityPath + collectionUUID - } - } + self: communityPath + collectionUUID, + }, + }, ); testItem = Object.assign(new Item(), { type: 'item', metadata: { - 'dc.title': [{ value: 'item' }] + 'dc.title': [{ value: 'item' }], }, uuid: itemUUID, owningCollection: createSuccessfulRemoteDataObject$(testCollection), _links: { owningCollection: collectionPath + collectionUUID, - self: itemPath + itemUUID - } - } + self: itemPath + itemUUID, + }, + }, ); dsoNameService = { getName: (dso) => getName(dso) }; @@ -93,8 +100,8 @@ describe('DSOBreadcrumbsService', () => { TestBed.configureTestingModule({ providers: [ { provide: LinkService, useValue: getMockLinkService() }, - { provide: DSONameService, useValue: dsoNameService } - ] + { provide: DSONameService, useValue: dsoNameService }, + ], }).compileComponents(); })); diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 9a22cd0e357..7d2e3697e18 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -1,27 +1,35 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + find, + map, + switchMap, +} from 'rxjs/operators'; + +import { getDSORoute } from '../../app-routing-paths'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { DSONameService } from './dso-name.service'; -import { Observable, of as observableOf } from 'rxjs'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; -import { LinkService } from '../cache/builders/link.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; +import { hasValue } from '../../shared/empty.util'; import { followLink } from '../../shared/utils/follow-link-config.model'; -import { find, map, switchMap } from 'rxjs/operators'; +import { LinkService } from '../cache/builders/link.service'; import { RemoteData } from '../data/remote-data'; -import { hasValue } from '../../shared/empty.util'; -import { Injectable } from '@angular/core'; -import { getDSORoute } from '../../app-routing-paths'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; +import { DSONameService } from './dso-name.service'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DSOBreadcrumbsService implements BreadcrumbsProviderService { constructor( protected linkService: LinkService, - protected dsoNameService: DSONameService + protected dsoNameService: DSONameService, ) { } @@ -46,7 +54,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsProviderService [...breadcrumbs, crumb]) + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]), ); } } diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 9f2f76599af..90b08dca143 100644 --- a/src/app/core/breadcrumbs/dso-name.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -22,7 +22,7 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return ['Person', Item, DSpaceObject]; - } + }, }); mockOrgUnitName = 'Molecular Spectroscopy'; @@ -32,7 +32,7 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return ['OrgUnit', Item, DSpaceObject]; - } + }, }); mockDSOName = 'Lorem Ipsum'; @@ -42,7 +42,7 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return [DSpaceObject]; - } + }, }); service = new DSONameService({ instant: (a) => a } as any); diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index 8e4fb771c64..988141209f4 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -1,7 +1,11 @@ import { Injectable } from '@angular/core'; -import { hasValue, isEmpty } from '../../shared/empty.util'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { TranslateService } from '@ngx-translate/core'; + +import { + hasValue, + isEmpty, +} from '../../shared/empty.util'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Metadata } from '../shared/metadata.utils'; /** @@ -9,7 +13,7 @@ import { Metadata } from '../shared/metadata.utils'; * on its render types. */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DSONameService { @@ -55,7 +59,7 @@ export class DSONameService { Default: (dso: DSpaceObject): string => { // If object doesn't have dc.title metadata use name property return dso.firstMetadataValue('dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); - } + }, }; /** diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index 0d1870487ab..a85338c4908 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -1,9 +1,9 @@ -import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { i18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; -describe('I18nBreadcrumbResolver', () => { +describe('i18nBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: I18nBreadcrumbResolver; + let resolver: any; let i18nBreadcrumbService: any; let i18nKey: string; let route: any; @@ -17,28 +17,28 @@ describe('I18nBreadcrumbResolver', () => { route = { data: { breadcrumbKey: i18nKey }, routeConfig: { - path: segment + path: segment, }, parent: { routeConfig: { - path: parentSegment - } - } as any + path: parentSegment, + }, + } as any, }; expectedPath = new URLCombiner(parentSegment, segment).toString(); i18nBreadcrumbService = {}; - resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); + resolver = i18nBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, {} as any); + const resolvedConfig = resolver(route, {} as any, i18nBreadcrumbService); const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, i18nBreadcrumbService); }).toThrow(); }); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index b3fadbbaa93..5f5c779211f 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,32 +1,31 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; import { hasNoValue } from '../../shared/empty.util'; import { currentPathFromSnapshot } from '../../shared/utils/route.utils'; +import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {I18nBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root' -}) -export class I18nBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { - } - - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const key = route.data.breadcrumbKey; - if (hasNoValue(key)) { - throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); - } - const fullPath = currentPathFromSnapshot(route); - return { provider: this.breadcrumbService, key: key, url: fullPath }; +export const i18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService), +): BreadcrumbConfig => { + const key = route.data.breadcrumbKey; + if (hasNoValue(key)) { + throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); } -} + const fullPath = currentPathFromSnapshot(route); + return { provider: breadcrumbService, key: key, url: fullPath }; +}; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts index ac2f2440370..3fcd911a464 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts @@ -1,7 +1,14 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { getTestScheduler } from 'jasmine-marbles'; -import { BREADCRUMB_MESSAGE_POSTFIX, I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + BREADCRUMB_MESSAGE_POSTFIX, + I18nBreadcrumbsService, +} from './i18n-breadcrumbs.service'; describe('I18nBreadcrumbsService', () => { let service: I18nBreadcrumbsService; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index 15563bdde8e..5746f6faf26 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -1,7 +1,11 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { Observable, of as observableOf } from 'rxjs'; -import { Injectable } from '@angular/core'; /** * The postfix for i18n breadcrumbs @@ -12,7 +16,7 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; * Service to calculate i18n breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class I18nBreadcrumbsService implements BreadcrumbsProviderService { diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index 3005b6f09ac..cb16cedb422 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -1,28 +1,35 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ItemDataService } from '../data/item-data.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root' -}) -export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; - } -} +export const itemBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: ItemDataService = inject(ItemDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts index 81b3b8ad2ae..a6bbe49ddd9 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; +import { navigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; -describe('NavigationBreadcrumbResolver', () => { +describe('navigationBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: NavigationBreadcrumbResolver; + let resolver: any; let NavigationBreadcrumbService: any; let i18nKey: string; let relatedI18nKey: string; @@ -18,33 +18,33 @@ describe('NavigationBreadcrumbResolver', () => { relatedRoutes: [ { path: '', - data: {breadcrumbKey: relatedI18nKey}, - } - ] + data: { breadcrumbKey: relatedI18nKey }, + }, + ], }, routeConfig: { - path: 'example' + path: 'example', }, parent: { routeConfig: { - path: '' + path: '', }, url: [{ - path: 'base' - }] - } as any + path: 'base', + }], + } as any, }; state = { - url: '/base/example' + url: '/base/example', }; expectedPath = '/base/example:/base'; NavigationBreadcrumbService = {}; - resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); + resolver = navigationBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, state); + const resolvedConfig = resolver(route, state, NavigationBreadcrumbService); const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts index 18ebfc395b7..ac306ee3f5c 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -1,52 +1,52 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route and related parents + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {NavigationBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root' -}) -export class NavigationBreadcrumbResolver implements Resolve> { - - private parentRoutes: ActivatedRouteSnapshot[] = []; - constructor(protected breadcrumbService: NavigationBreadcrumbsService) { - } +export const navigationBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService), +): BreadcrumbConfig => { + const parentRoutes: ActivatedRouteSnapshot[] = []; + getParentRoutes(route, parentRoutes); + const relatedRoutes = route.data.relatedRoutes; + const parentPaths = parentRoutes.map(parent => parent.routeConfig?.path); + const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); + const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; + const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); - /** - * Method to collect all parent routes snapshot from current route snapshot - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - */ - private getParentRoutes(route: ActivatedRouteSnapshot): void { - if (route.parent) { - this.parentRoutes.push(route.parent); - this.getParentRoutes(route.parent); - } - } - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - this.getParentRoutes(route); - const relatedRoutes = route.data.relatedRoutes; - const parentPaths = this.parentRoutes.map(parent => parent.routeConfig?.path); - const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); - const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; - const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); + const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${current.data.breadcrumbKey}`; + }, route.data.breadcrumbKey); + const combinedUrls = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${baseUrl}${current.path}`; + }, state.url); - const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${current.data.breadcrumbKey}`; - }, route.data.breadcrumbKey); - const combinedUrls = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${baseUrl}${current.path}`; - }, state.url); + return { provider: breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; +}; - return {provider: this.breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls}; +/** + * Method to collect all parent routes snapshot from current route snapshot + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {ActivatedRouteSnapshot[]} parentRoutes + */ +function getParentRoutes(route: ActivatedRouteSnapshot, parentRoutes: ActivatedRouteSnapshot[]): void { + if (route.parent) { + parentRoutes.push(route.parent); + getParentRoutes(route.parent, parentRoutes); } } diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts index beebeed94e1..2da8b06eab7 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts @@ -1,7 +1,11 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { Observable, of as observableOf } from 'rxjs'; -import { Injectable } from '@angular/core'; /** * The postfix for i18n breadcrumbs @@ -12,7 +16,7 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; * Service to calculate i18n breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NavigationBreadcrumbsService implements BreadcrumbsProviderService { diff --git a/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts index 98e20e285d9..646b967fe5b 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts @@ -1,6 +1,10 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { getTestScheduler } from 'jasmine-marbles'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BREADCRUMB_MESSAGE_POSTFIX } from './i18n-breadcrumbs.service'; import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; @@ -35,9 +39,9 @@ describe('NavigationBreadcrumbsService', () => { it('should return an array of breadcrumbs based on strings by adding the postfix', () => { const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL); getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [ - new Breadcrumb(childrenString + BREADCRUMB_MESSAGE_POSTFIX, childrenUrl), - new Breadcrumb(parentString + BREADCRUMB_MESSAGE_POSTFIX, parentUrl), - ].reverse() }); + new Breadcrumb(childrenString + BREADCRUMB_MESSAGE_POSTFIX, childrenUrl), + new Breadcrumb(parentString + BREADCRUMB_MESSAGE_POSTFIX, parentUrl), + ].reverse() }); }); }); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts index b6f41424693..7c2c34d4790 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; +import { publicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; -describe('PublicationClaimBreadcrumbResolver', () => { +describe('publicationClaimBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: PublicationClaimBreadcrumbResolver; + let resolver: any; let publicationClaimBreadcrumbService: any; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; @@ -16,14 +16,14 @@ describe('PublicationClaimBreadcrumbResolver', () => { return this[param]; }, targetId: expectedId, - } + }, }; publicationClaimBreadcrumbService = {}; - resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); + resolver = publicationClaimBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, {url: fullPath } as any); + const resolvedConfig = resolver(route as any, { url: fullPath } as any, publicationClaimBreadcrumbService); const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts index 713500d6a73..a1b52ce333f 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -1,24 +1,18 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import {BreadcrumbConfig} from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; -@Injectable({ - providedIn: 'root' -}) -export class PublicationClaimBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { - } +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; - /** - * Method that resolve Publication Claim item into a breadcrumb - * The parameter are retrieved by the url since part of the Publication Claim route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const targetId = route.paramMap.get('targetId').split(':')[1]; - return { provider: this.breadcrumbService, key: targetId }; - } -} +export const publicationClaimBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService), +): BreadcrumbConfig => { + const targetId = route.paramMap.get('targetId').split(':')[1]; + return { provider: breadcrumbService, key: targetId }; +}; diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts index 11062210bb3..8424b5edda8 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts @@ -1,14 +1,18 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { getTestScheduler } from 'jasmine-marbles'; -import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { of } from 'rxjs'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; + describe('PublicationClaimBreadcrumbService', () => { let service: PublicationClaimBreadcrumbService; let dsoNameService: any = { - getName: (str) => str + getName: (str) => str, }; let translateService: any = { instant: (str) => str, @@ -44,7 +48,7 @@ describe('PublicationClaimBreadcrumbService', () => { it('should return a breadcrumb based on a string', () => { const breadcrumbs = service.getBreadcrumbs(exampleKey); getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY, ADMIN_PUBLICATION_CLAIMS_PATH), - new Breadcrumb(exampleKey, undefined)] + new Breadcrumb(exampleKey, undefined)], }); }); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts index 1a87fd7de60..43b7ed5761b 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts @@ -1,22 +1,24 @@ -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { combineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import { ItemDataService } from '../data/item-data.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { map } from 'rxjs/operators'; -import { DSONameService } from './dso-name.service'; import { TranslateService } from '@ngx-translate/core'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { FeatureID } from '../data/feature-authorization/feature-id'; - - +import { ItemDataService } from '../data/item-data.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; +import { DSONameService } from './dso-name.service'; /** * Service to calculate Publication claims breadcrumbs */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderService { private ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim'; @@ -38,9 +40,9 @@ export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderSer map(([item, isAdmin]) => { const itemName = this.dsoNameService.getName(item.payload); return isAdmin ? [new Breadcrumb(this.tranlsateService.instant(this.ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY), this.ADMIN_PUBLICATION_CLAIMS_PATH), - new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)] : - [new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)]; - }) + new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', { name: itemName }), undefined)] : + [new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', { name: itemName }), undefined)]; + }), ); } } diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts index 3544af62e7a..fe2fe77e7f4 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import {QualityAssuranceBreadcrumbResolver} from './quality-assurance-breadcrumb.resolver'; +import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; -describe('QualityAssuranceBreadcrumbResolver', () => { +describe('qualityAssuranceBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: QualityAssuranceBreadcrumbResolver; + let resolver: any; let qualityAssuranceBreadcrumbService: any; let route: any; const fullPath = '/test/quality-assurance/'; @@ -15,15 +15,15 @@ describe('QualityAssuranceBreadcrumbResolver', () => { return this[param]; }, sourceId: 'testSourceId', - topicId: 'testTopicId' - } + topicId: 'testTopicId', + }, }; qualityAssuranceBreadcrumbService = {}; - resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); + resolver = qualityAssuranceBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, {url: fullPath + 'testSourceId'} as any); + const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, qualityAssuranceBreadcrumbService); const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts index 6eb351ab1ab..6507a75de66 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -1,32 +1,27 @@ -import { Injectable } from '@angular/core'; -import {QualityAssuranceBreadcrumbService} from './quality-assurance-breadcrumb.service'; -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; -import {BreadcrumbConfig} from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; -@Injectable({ - providedIn: 'root' -}) -export class QualityAssuranceBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; - /** - * Method that resolve QA item into a breadcrumb - * The parameter are retrieved by the url since part of the QA route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const sourceId = route.paramMap.get('sourceId'); - const topicId = route.paramMap.get('topicId'); - let key = sourceId; +export const qualityAssuranceBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService), +): BreadcrumbConfig => { + const sourceId = route.paramMap.get('sourceId'); + const topicId = route.paramMap.get('topicId'); + let key = sourceId; - if (topicId) { - key += `:${topicId}`; - } - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(sourceId)); - - return { provider: this.breadcrumbService, key, url }; + if (topicId) { + key += `:${topicId}`; } -} + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(sourceId)); + + return { provider: breadcrumbService, key, url }; +}; diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts index cefa1d2f6fb..f8d30754cad 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts @@ -1,7 +1,11 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { getTestScheduler } from 'jasmine-marbles'; -import {QualityAssuranceBreadcrumbService} from './quality-assurance-breadcrumb.service'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; describe('QualityAssuranceBreadcrumbService', () => { let service: QualityAssuranceBreadcrumbService; @@ -32,7 +36,7 @@ describe('QualityAssuranceBreadcrumbService', () => { it('should return a breadcrumb based on a string', () => { const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL); getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleQaKey, exampleURL), - new Breadcrumb(exampleString, exampleURL + exampleString)] + new Breadcrumb(exampleString, exampleURL + exampleString)], }); }); }); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts index a0299705a40..580a5e5f8ee 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts @@ -1,16 +1,18 @@ -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; - +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; /** * Service to calculate QA breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderService { diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index f321c2551cd..affa63a5480 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -1,11 +1,12 @@ -import { BrowseDefinitionDataService } from './browse-definition-data.service'; -import { followLink } from '../../shared/utils/follow-link-config.model'; import { EMPTY } from 'rxjs'; -import { FindListOptions } from '../data/find-list-options.model'; + +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { RequestService } from '../data/request.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../data/find-list-options.model'; +import { RequestService } from '../data/request.service'; +import { BrowseDefinitionDataService } from './browse-definition-data.service'; describe(`BrowseDefinitionDataService`, () => { let requestService: RequestService; @@ -18,7 +19,7 @@ describe(`BrowseDefinitionDataService`, () => { const options = new FindListOptions(); const linksToFollow = [ followLink('entries'), - followLink('items') + followLink('items'), ]; function initTestService() { diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index bc495a51f4f..9c0d0d16c95 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -1,33 +1,45 @@ // eslint-disable-next-line max-classes-per-file import { Injectable } from '@angular/core'; -import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; -import { RequestService } from '../data/request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list.model'; -import { FindListOptions } from '../data/find-list-options.model'; +import { + FindAllData, + FindAllDataImpl, +} from '../data/base/find-all-data'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data'; -import { dataService } from '../data/base/data-service.decorator'; -import { isNotEmpty, isNotEmptyOperator, hasValue } from '../../shared/empty.util'; -import { take } from 'rxjs/operators'; +import { + SearchData, + SearchDataImpl, +} from '../data/base/search-data'; +import { FindListOptions } from '../data/find-list-options.model'; +import { PaginatedList } from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; import { BrowseDefinitionRestRequest } from '../data/request.models'; -import { RequestParam } from '../cache/models/request-param.model'; -import { SearchData, SearchDataImpl } from '../data/base/search-data'; +import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; /** * Create a GET request for the given href, and send it. * Use a GET request specific for BrowseDefinitions. */ export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestService, - responseMsToLive: number, - href$: string | Observable, - useCachedVersionIfAvailable: boolean = true): void => { + responseMsToLive: number, + href$: string | Observable, + useCachedVersionIfAvailable: boolean = true): void => { if (isNotEmpty(href$)) { if (typeof href$ === 'string') { href$ = observableOf(href$); @@ -35,7 +47,7 @@ export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestS href$.pipe( isNotEmptyOperator(), - take(1) + take(1), ).subscribe((href: string) => { const requestId = requestService.generateRequestId(); const request = new BrowseDefinitionRestRequest(requestId, href); @@ -62,7 +74,6 @@ class BrowseDefinitionFindAllDataImpl extends FindAllDataImpl @Injectable({ providedIn: 'root', }) -@dataService(BROWSE_DEFINITION) export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData, SearchData { private findAllData: BrowseDefinitionFindAllDataImpl; private searchData: SearchDataImpl; @@ -150,7 +161,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService { let scheduler: TestScheduler; @@ -31,26 +42,26 @@ describe('BrowseService', () => { sortOptions: [ { name: 'title', - metadata: 'dc.title' + metadata: 'dc.title', }, { name: 'dateissued', - metadata: 'dc.date.issued' + metadata: 'dc.date.issued', }, { name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } + metadata: 'dc.date.accessioned', + }, ], defaultSortOrder: 'ASC', type: 'browse', metadataKeys: [ - 'dc.date.issued' + 'dc.date.issued', ], _links: { self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } - } + items: { href: 'https://rest.api/discover/browses/dateissued/items' }, + }, }), Object.assign(new ValueListBrowseDefinition(), { id: 'author', @@ -58,28 +69,28 @@ describe('BrowseService', () => { sortOptions: [ { name: 'title', - metadata: 'dc.title' + metadata: 'dc.title', }, { name: 'dateissued', - metadata: 'dc.date.issued' + metadata: 'dc.date.issued', }, { name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } + metadata: 'dc.date.accessioned', + }, ], defaultSortOrder: 'ASC', type: 'browse', metadataKeys: [ 'dc.contributor.*', - 'dc.creator' + 'dc.creator', ], _links: { self: { href: 'https://rest.api/discover/browses/author' }, entries: { href: 'https://rest.api/discover/browses/author/entries' }, - items: { href: 'https://rest.api/discover/browses/author/items' } - } + items: { href: 'https://rest.api/discover/browses/author/items' }, + }, }), Object.assign(new HierarchicalBrowseDefinition(), { id: 'srsc', @@ -88,14 +99,14 @@ describe('BrowseService', () => { vocabulary: 'srsc', type: 'browse', metadata: [ - 'dc.subject' + 'dc.subject', ], _links: { vocabulary: { 'href': 'https://rest.api/submission/vocabularies/srsc/' }, items: { 'href': 'https://rest.api/discover/browses/srsc/items' }, entries: { 'href': 'https://rest.api/discover/browses/srsc/entries' }, - self: { 'href': 'https://rest.api/discover/browses/srsc' } - } + self: { 'href': 'https://rest.api/discover/browses/srsc' }, + }, }), ]; @@ -104,13 +115,13 @@ describe('BrowseService', () => { const getRequestEntry$ = (successful: boolean) => { return observableOf({ - response: { isSuccessful: successful, payload: browseDefinitions } as any + response: { isSuccessful: successful, payload: browseDefinitions } as any, } as RequestEntry); }; function initTestService() { browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)) + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)), }); hrefOnlyDataService = getMockHrefOnlyDataService(); return new BrowseService( @@ -118,7 +129,7 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService + rdbService, ); } @@ -164,7 +175,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); @@ -178,7 +189,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); @@ -193,7 +204,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); }); @@ -208,7 +219,7 @@ describe('BrowseService', () => { service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { - a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) + a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)), })); }); @@ -290,7 +301,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expectedURL + a: expectedURL, })); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 58bbc0b8700..43f33f26e49 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,39 +1,51 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { + distinctUntilChanged, + map, + startWith, +} from 'rxjs/operators'; + +import { + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SortDirection } from '../cache/models/sort-options.model'; +import { HrefOnlyDataService } from '../data/href-only-data.service'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; -import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; +import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { getBrowseDefinitionLinks, getFirstOccurrence, - getRemoteDataPayload, getFirstSucceededRemoteData, - getPaginatedListPayload + getPaginatedListPayload, + getRemoteDataPayload, } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -import { HrefOnlyDataService } from '../data/href-only-data.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; -import { SortDirection } from '../cache/models/sort-options.model'; - +import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('thumbnail') + followLink('thumbnail'), ]; /** * The service handling all browse requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class BrowseService { protected linkPath = 'browses'; @@ -102,7 +114,7 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }) + }), ); if (options.fetchThumbnail ) { return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); @@ -187,12 +199,12 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }) + }), ); return this.hrefOnlyDataService.findListByHref(href$).pipe( getFirstSucceededRemoteData(), - getFirstOccurrence() + getFirstOccurrence(), ); } @@ -248,7 +260,7 @@ export class BrowseService { } return isNotEmpty(matchingKeys); - }) + }), ), map((def: BrowseDefinition) => { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { @@ -258,7 +270,7 @@ export class BrowseService { } }), startWith(undefined), - distinctUntilChanged() + distinctUntilChanged(), ); } diff --git a/src/app/core/cache/builders/build-decorators.spec.ts b/src/app/core/cache/builders/build-decorators.spec.ts index e4baaa4a5a8..53f4cb2f7f3 100644 --- a/src/app/core/cache/builders/build-decorators.spec.ts +++ b/src/app/core/cache/builders/build-decorators.spec.ts @@ -1,7 +1,12 @@ import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import { dataService, getDataServiceFor, getLinkDefinition, link } from './build-decorators'; +import { + dataService, + getDataServiceFor, + getLinkDefinition, + link, +} from './build-decorators'; class TestHALResource implements HALResource { _links: { diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 8da1861ecf4..be3ffc0f4d7 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,26 +1,27 @@ -import { hasNoValue, hasValue } from '../../../shared/empty.util'; +import { InjectionToken } from '@angular/core'; +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import { - getResourceTypeValueFor -} from '../object-cache.reducer'; -import { InjectionToken } from '@angular/core'; import { CacheableObject } from '../cacheable-object.model'; +import { getResourceTypeValueFor } from '../object-cache.reducer'; import { TypedObject } from '../typed-object.model'; export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>('getDataServiceFor', { providedIn: 'root', - factory: () => getDataServiceFor + factory: () => getDataServiceFor, }); export const LINK_DEFINITION_FACTORY = new InjectionToken<(source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition>('getLinkDefinition', { providedIn: 'root', - factory: () => getLinkDefinition + factory: () => getLinkDefinition, }); export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<(source: GenericConstructor) => Map>>('getLinkDefinitions', { providedIn: 'root', - factory: () => getLinkDefinitions + factory: () => getLinkDefinitions, }); const resolvedLinkKey = Symbol('resolvedLink'); @@ -122,7 +123,7 @@ export const link = ( resourceType, isList, linkName, - propertyName + propertyName, }); linkMap.set(target.constructor, targetMap); diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 0ddfe058701..d9878913888 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -1,15 +1,21 @@ /* eslint-disable max-classes-per-file */ -import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { + isEmpty, + take, +} from 'rxjs/operators'; + +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; +import { TestDataService } from '../../../shared/testing/test-data-service.mock'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; +import { + LINK_DEFINITION_FACTORY, + LINK_DEFINITION_MAP_FACTORY, +} from './build-decorators'; import { LinkService } from './link.service'; -import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators'; -import { isEmpty } from 'rxjs/operators'; -import { FindListOptions } from '../../data/find-list-options.model'; -import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; const TEST_MODEL = new ResourceType('testmodel'); let result: any; @@ -31,16 +37,9 @@ class TestModel implements HALResource { successor?: TestModel; } -@Injectable() -class TestDataService { - findListByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { - return 'findListByHref'; - } - - findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { - return 'findByHref'; - } -} +const mockDataServiceMap: any = new Map([ + [TEST_MODEL.value, () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService)], +]); let testDataService: TestDataService; @@ -54,48 +53,54 @@ describe('LinkService', () => { value: 'a test value', _links: { self: { - href: 'http://self.link' + href: 'http://self.link', }, predecessor: { - href: 'http://predecessor.link' + href: 'http://predecessor.link', }, successor: { - href: 'http://successor.link' + href: 'http://successor.link', }, - } + }, }); testDataService = new TestDataService(); spyOn(testDataService, 'findListByHref').and.callThrough(); spyOn(testDataService, 'findByHref').and.callThrough(); TestBed.configureTestingModule({ - providers: [LinkService, { - provide: TestDataService, - useValue: testDataService - }, { - provide: DATA_SERVICE_FACTORY, - useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService), - }, { - provide: LINK_DEFINITION_FACTORY, - useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor' - }), - }, { - provide: LINK_DEFINITION_MAP_FACTORY, - useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([ - { + providers: [ + LinkService, + { + provide: TestDataService, + useValue: testDataService, + }, + { + provide: APP_DATA_SERVICES_MAP, + useValue: mockDataServiceMap, + }, + { + provide: LINK_DEFINITION_FACTORY, + useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({ resourceType: TEST_MODEL, linkName: 'predecessor', propertyName: 'predecessor', - }, - { - resourceType: TEST_MODEL, - linkName: 'successor', - propertyName: 'successor', - } - ]), - }] + }), + }, + { + provide: LINK_DEFINITION_MAP_FACTORY, + useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([ + { + resourceType: TEST_MODEL, + linkName: 'predecessor', + propertyName: 'predecessor', + }, + { + resourceType: TEST_MODEL, + linkName: 'successor', + propertyName: 'successor', + }, + ]), + }, + ], }); service = TestBed.inject(LinkService); }); @@ -103,10 +108,13 @@ describe('LinkService', () => { describe('resolveLink', () => { describe(`when the linkdefinition concerns a single object`, () => { beforeEach(() => { - service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); + result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); }); - it('should call dataservice.findByHref with the correct href and nested links', () => { - expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor')); + it('should call dataservice.findByHref with the correct href and nested links', (done) => { + result.predecessor.pipe(take(1)).subscribe(() => { + expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor')); + done(); + }); }); }); describe(`when the linkdefinition concerns a list`, () => { @@ -115,12 +123,15 @@ describe('LinkService', () => { resourceType: TEST_MODEL, linkName: 'predecessor', propertyName: 'predecessor', - isList: true + isList: true, }); - service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor'))); + result = service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor'))); }); - it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', () => { - expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor')); + it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', (done) => { + result.predecessor.pipe(take(1)).subscribe((res) => { + expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor')); + done(); + }); }); }); describe('either way', () => { @@ -132,15 +143,14 @@ describe('LinkService', () => { expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor'); }); - it('should call getDataServiceFor with the correct resource type', () => { - expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL); - }); - - it('should return the model with the resolved link', () => { + it('should return the model with the resolved link', (done) => { expect(result.type).toBe(TEST_MODEL); expect(result.value).toBe('a test value'); expect(result._links.self.href).toBe('http://self.link'); - expect(result.predecessor).toBe('findByHref'); + result.predecessor.subscribe((res) => { + expect(res).toBe('findByHref'); + done(); + }); }); }); @@ -157,12 +167,16 @@ describe('LinkService', () => { describe(`when there is no dataservice for the resourcetype in the link`, () => { beforeEach(() => { - ((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined); + (service as any).map = {}; }); - it('should throw an error', () => { - expect(() => { - service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); - }).toThrow(); + it('should throw an error', (done) => { + result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); + result.predecessor.subscribe({ + error: (error: unknown) => { + expect(error).toBeDefined(); + done(); + }, + }); }); }); }); @@ -213,12 +227,12 @@ describe('LinkService', () => { value: 'a test value', _links: { self: { - href: 'http://self.link' + href: 'http://self.link', }, predecessor: { - href: 'http://predecessor.link' - } - } + href: 'http://predecessor.link', + }, + }, }); }); @@ -227,8 +241,11 @@ describe('LinkService', () => { result = service.resolveLinks(testModel, followLink('predecessor')); }); - it('should return the model with the resolved link', () => { - expect(result.predecessor).toBe('findByHref'); + it('should return the model with the resolved link', (done) => { + result.predecessor.subscribe((res) => { + expect(res).toBe('findByHref'); + done(); + }); }); }); @@ -237,7 +254,7 @@ describe('LinkService', () => { ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({ resourceType: TEST_MODEL, linkName: 'successor', - propertyName: 'successor' + propertyName: 'successor', }); result = service.resolveLinks(testModel, followLink('successor')); }); diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index afc7ab88e40..6265e89d532 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -1,32 +1,48 @@ -import { Inject, Injectable, Injector } from '@angular/core'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { + Inject, + Injectable, + Injector, +} from '@angular/core'; +import { + EMPTY, + Observable, +} from 'rxjs'; +import { + catchError, + switchMap, +} from 'rxjs/operators'; + +import { + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../../config/app-config.interface'; +import { + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { HALDataService } from '../../data/base/hal-data-service.interface'; +import { PaginatedList } from '../../data/paginated-list.model'; +import { RemoteData } from '../../data/remote-data'; +import { lazyDataService } from '../../lazy-data-service'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; -import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY, LinkDefinition, } from './build-decorators'; -import { RemoteData } from '../../data/remote-data'; -import { EMPTY, Observable } from 'rxjs'; -import { ResourceType } from '../../shared/resource-type'; -import { HALDataService } from '../../data/base/hal-data-service.interface'; -import { PaginatedList } from '../../data/paginated-list.model'; /** * A Service to handle the resolving and removing * of resolved {@link HALLink}s on HALResources */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class LinkService { constructor( - protected parentInjector: Injector, - @Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor>, + protected injector: Injector, + @Inject(APP_DATA_SERVICES_MAP) private map: LazyDataServicesMap, @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: (source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition, @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: (source: GenericConstructor) => Map>, ) { @@ -55,34 +71,32 @@ export class LinkService { */ public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable>> { const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name); - if (hasValue(matchingLinkDef)) { - const provider = this.getDataServiceFor(matchingLinkDef.resourceType); + const lazyProvider$: Observable> = lazyDataService(this.map, matchingLinkDef.resourceType.value, this.injector); + return lazyProvider$.pipe( + switchMap((provider: HALDataService) => { + const link = model._links[matchingLinkDef.linkName]; + if (hasValue(link)) { + const href = link.href; - if (hasNoValue(provider)) { - throw new Error(`The @link() for ${String(linkToFollow.name)} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`); - } - - const service: HALDataService = Injector.create({ - providers: [], - parent: this.parentInjector, - }).get(provider); - - const link = model._links[matchingLinkDef.linkName]; - if (hasValue(link)) { - const href = link.href; - - try { - if (matchingLinkDef.isList) { - return service.findListByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); - } else { - return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + try { + if (matchingLinkDef.isList) { + return provider.findListByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + } else { + return provider.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + } + } catch (e) { + console.error(`Something went wrong when using ${matchingLinkDef.resourceType.value}) ${hasValue(provider) ? '' : '(undefined) '}to resolve link ${String(linkToFollow.name)} at ${href}`); + throw e; + } } - } catch (e) { - console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${String(linkToFollow.name)} at ${href}`); - throw e; - } - } + + return EMPTY; + }), + catchError((err: unknown) => { + throw new Error(`The @link() for ${String(linkToFollow.name)} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`); + }), + ); } else if (!linkToFollow.isOptional) { throw new Error(`followLink('${String(linkToFollow.name)}') was used as a required link for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${String(linkToFollow.name)}`); } diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index d9b856bb777..ec756ce85eb 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -1,26 +1,43 @@ -import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model'; -import { Item } from '../../shared/item.model'; -import { PageInfo } from '../../shared/page-info.model'; -import { RemoteDataBuildService } from './remote-data-build.service'; -import { ObjectCacheService } from '../object-cache.service'; -import { ITEM } from '../../shared/item.resource-type'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { take } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; + import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; -import { LinkService } from './link.service'; -import { RequestService } from '../../data/request.service'; -import { UnCacheableObject } from '../../shared/uncacheable-object.model'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { + createFailedRemoteDataObject, + createPendingRemoteDataObject, + createSuccessfulRemoteDataObject, +} from '../../../shared/remote-data.utils'; +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; -import { Observable, of as observableOf } from 'rxjs'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { take } from 'rxjs/operators'; -import { HALLink } from '../../shared/hal-link.model'; -import { RequestEntryState } from '../../data/request-entry-state.model'; +import { RequestService } from '../../data/request.service'; import { RequestEntry } from '../../data/request-entry.model'; -import { cold } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { fakeAsync, tick } from '@angular/core/testing'; +import { RequestEntryState } from '../../data/request-entry-state.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { Item } from '../../shared/item.model'; +import { ITEM } from '../../shared/item.resource-type'; +import { PageInfo } from '../../shared/page-info.model'; +import { UnCacheableObject } from '../../shared/uncacheable-object.model'; +import { ObjectCacheService } from '../object-cache.service'; +import { LinkService } from './link.service'; +import { RemoteDataBuildService } from './remote-data-build.service'; describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; @@ -49,7 +66,7 @@ describe('RemoteDataBuildService', () => { linkService = getMockLinkService(); requestService = getMockRequestService(); unCacheableObject = { - foo: 'bar' + foo: 'bar', }; pageInfo = new PageInfo(); selfLink1 = 'https://rest.api/some/object'; @@ -64,31 +81,31 @@ describe('RemoteDataBuildService', () => { 'dc.title': [ { language: 'en_US', - value: 'Item nr 1' - } - ] + value: 'Item nr 1', + }, + ], }, _links: { self: { - href: selfLink1 - } - } + href: selfLink1, + }, + }, }), Object.assign(new Item(), { metadata: { 'dc.title': [ { language: 'en_US', - value: 'Item nr 2' - } - ] + value: 'Item nr 2', + }, + ], }, _links: { self: { - href: selfLink2 - } - } - }) + href: selfLink2, + }, + }, + }), ]; paginatedList = buildPaginatedList(pageInfo, array); normalizedPaginatedList = buildPaginatedList(pageInfo, array, true); @@ -96,43 +113,43 @@ describe('RemoteDataBuildService', () => { paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); entrySuccessCacheable = { request: { - uuid: '17820127-0ee5-4ed4-b6da-e654bdff8487' + uuid: '17820127-0ee5-4ed4-b6da-e654bdff8487', }, state: RequestEntryState.Success, response: { statusCode: 200, payloadLink: { - href: selfLink1 - } - } + href: selfLink1, + }, + }, } as RequestEntry; entrySuccessUnCacheable = { request: { - uuid: '0aa5ec06-d6a7-4e73-952e-1e0462bd1501' + uuid: '0aa5ec06-d6a7-4e73-952e-1e0462bd1501', }, state: RequestEntryState.Success, response: { statusCode: 200, unCacheableObject, - } + }, } as RequestEntry; entrySuccessNoContent = { request: { - uuid: '780a7295-6102-4a43-9775-80f2a4ff673c' + uuid: '780a7295-6102-4a43-9775-80f2a4ff673c', }, state: RequestEntryState.Success, response: { - statusCode: 204 + statusCode: 204, }, } as RequestEntry; entryError = { request: { - uuid: '1609dcbc-8442-4877-966e-864f151cc40c' + uuid: '1609dcbc-8442-4877-966e-864f151cc40c', }, state: RequestEntryState.Error, response: { statusCode: 500, - } + }, } as RequestEntry; requestEntry$ = observableOf(entrySuccessCacheable); linksToFollow = [ @@ -427,8 +444,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - payloadLink: { href: 'payload-link' } - } + payloadLink: { href: 'payload-link' }, + }, }; }); @@ -441,8 +458,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - payloadLink: undefined - } + payloadLink: undefined, + }, }; }); @@ -459,8 +476,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - unCacheableObject: Object.assign({}) - } + unCacheableObject: Object.assign({}), + }, }; }); @@ -472,7 +489,7 @@ describe('RemoteDataBuildService', () => { describe('when the entry\'s response doesn\'t contain an uncacheable object', () => { beforeEach(() => { entry = { - response: {} + response: {}, }; }); @@ -487,7 +504,7 @@ describe('RemoteDataBuildService', () => { it(`should return a new instance of that type`, () => { const source: any = { type: ITEM, - uuid: 'some-uuid' + uuid: 'some-uuid', }; const result = (service as any).plainObjectToInstance(source); @@ -503,7 +520,7 @@ describe('RemoteDataBuildService', () => { it(`should return a new plain JS object`, () => { const source: any = { type: 'foobar', - uuid: 'some-uuid' + uuid: 'some-uuid', }; const result = (service as any).plainObjectToInstance(source); @@ -528,7 +545,7 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { paginatedLinksToFollow = [ followLink('page', {}, ...linksToFollow), - ...linksToFollow + ...linksToFollow, ]; }); describe(`and the given list doesn't have a page property already`, () => { @@ -843,15 +860,15 @@ describe('RemoteDataBuildService', () => { it('should only emit after the callback is done', () => { testScheduler.run(({ cold: tsCold, expectObservable }) => { buildFromRequestUUIDSpy.and.returnValue( - tsCold('-p----s', RDs) + tsCold('-p----s', RDs), ); callback.and.returnValue( - tsCold(' --t', BOOLEAN) + tsCold(' --t', BOOLEAN), ); const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback); expectObservable(done$).toBe( - ' -p------s', RDs // resulting duration between pending & successful includes the callback + ' -p------s', RDs, // resulting duration between pending & successful includes the callback ); }); }); diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index cd63ff64366..36305b4a0c4 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,29 +5,52 @@ import { Observable, of as observableOf, } from 'rxjs'; -import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators'; -import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util'; +import { + distinctUntilKeyChanged, + filter, + map, + startWith, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, + isUndefined, +} from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list.model'; +import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; import { RemoteData } from '../../data/remote-data'; import { RequestService } from '../../data/request.service'; -import { ObjectCacheService } from '../object-cache.service'; -import { LinkService } from './link.service'; -import { HALLink } from '../../shared/hal-link.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { getClassForType } from './build-decorators'; -import { HALResource } from '../../shared/hal-resource.model'; -import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; -import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; -import { getResourceTypeValueFor } from '../object-cache.reducer'; -import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-entry-state.model'; -import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators'; import { RequestEntry } from '../../data/request-entry.model'; +import { + hasSucceeded, + isStale, + RequestEntryState, +} from '../../data/request-entry-state.model'; import { ResponseState } from '../../data/response-state.model'; +import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { HALLink } from '../../shared/hal-link.model'; +import { HALResource } from '../../shared/hal-resource.model'; import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { + getRequestFromRequestHref, + getRequestFromRequestUUID, +} from '../../shared/request.operators'; +import { getResourceTypeValueFor } from '../object-cache.reducer'; +import { ObjectCacheService } from '../object-cache.service'; +import { getClassForType } from './build-decorators'; +import { LinkService } from './link.service'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, protected linkService: LinkService, @@ -77,7 +100,7 @@ export class RemoteDataBuildService { } } return [obj]; - }) + }), ); } @@ -151,7 +174,7 @@ export class RemoteDataBuildService { paginatedList.page = page .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => - this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow), ); if (isNotEmpty(otherLinks)) { return this.linkService.resolveLinks(paginatedList, ...otherLinks); @@ -164,7 +187,7 @@ export class RemoteDataBuildService { .filter((obj: any) => obj != null) .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => - this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow), ); if (isNotEmpty(otherLinks)) { return observableOf(this.linkService.resolveLinks(paginatedList, ...otherLinks)); @@ -230,7 +253,7 @@ export class RemoteDataBuildService { } else { return [rd]; } - }) + }), ); } @@ -295,12 +318,12 @@ export class RemoteDataBuildService { toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { return observableCombineLatest([ requestEntry$, - payload$ + payload$, ]).pipe( filter(([entry,payload]: [RequestEntry, T]) => hasValue(entry) && // filter out cases where the state is successful, but the payload isn't yet set - !(hasSucceeded(entry.state) && isUndefined(payload)) + !(hasSucceeded(entry.state) && isUndefined(payload)), ), map(([entry, payload]: [RequestEntry, T]) => { let response = entry.response; @@ -315,9 +338,9 @@ export class RemoteDataBuildService { entry.state, response.errorMessage, payload, - response.statusCode + response.statusCode, ); - }) + }), ); } @@ -408,7 +431,7 @@ export class RemoteDataBuildService { state, errorMessage, payload, - statusCode + statusCode, ); })); } diff --git a/src/app/core/cache/cacheable-object.model.ts b/src/app/core/cache/cacheable-object.model.ts index b7d1609d585..86d041dab77 100644 --- a/src/app/core/cache/cacheable-object.model.ts +++ b/src/app/core/cache/cacheable-object.model.ts @@ -1,6 +1,6 @@ /* tslint:disable:max-classes-per-file */ -import { HALResource } from '../shared/hal-resource.model'; import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { TypedObject } from './typed-object.model'; /** diff --git a/src/app/core/cache/models/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts index a87acdd5065..903a7794951 100644 --- a/src/app/core/cache/models/self-link.model.ts +++ b/src/app/core/cache/models/self-link.model.ts @@ -3,9 +3,9 @@ import { autoserialize } from 'cerialize'; export class SelfLink { @autoserialize - self: string; + self: string; @autoserialize - uuid: string; + uuid: string; } diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index c18a20ffd63..5f8f60e1f1f 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -1,8 +1,8 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { type } from '../../shared/ngrx/type'; -import { Operation } from 'fast-json-patch'; import { CacheableObject } from './cacheable-object.model'; /** @@ -15,7 +15,7 @@ export const ObjectCacheActionTypes = { ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'), APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH'), ADD_DEPENDENTS: type('dspace/core/cache/object/ADD_DEPENDENTS'), - REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS') + REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS'), }; /** diff --git a/src/app/core/cache/object-cache.effects.spec.ts b/src/app/core/cache/object-cache.effects.spec.ts index 3a50a5dbc74..66270be4c26 100644 --- a/src/app/core/cache/object-cache.effects.spec.ts +++ b/src/app/core/cache/object-cache.effects.spec.ts @@ -1,10 +1,14 @@ import { TestBed } from '@angular/core/testing'; -import { Observable } from 'rxjs'; import { provideMockActions } from '@ngrx/effects/testing'; -import { cold, hot } from 'jasmine-marbles'; -import { ObjectCacheEffects } from './object-cache.effects'; -import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { Observable } from 'rxjs'; + import { StoreActionTypes } from '../../store.actions'; +import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; +import { ObjectCacheEffects } from './object-cache.effects'; describe('ObjectCacheEffects', () => { let cacheEffects: ObjectCacheEffects; diff --git a/src/app/core/cache/object-cache.effects.ts b/src/app/core/cache/object-cache.effects.ts index fa2bf6f6902..0de59a152cb 100644 --- a/src/app/core/cache/object-cache.effects.ts +++ b/src/app/core/cache/object-cache.effects.ts @@ -1,6 +1,10 @@ -import { map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { map } from 'rxjs/operators'; import { StoreActionTypes } from '../../store.actions'; import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; @@ -16,9 +20,9 @@ export class ObjectCacheEffects { * This assumes that the server cached everything a negligible * time ago, and will likely need to be revisited later */ - fixTimestampsOnRehydrate = createEffect(() => this.actions$ + fixTimestampsOnRehydrate = createEffect(() => this.actions$ .pipe(ofType(StoreActionTypes.REHYDRATE), - map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())) + map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())), )); constructor(private actions$: Actions) { diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 919edc8e577..7dda02a0f5d 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { Operation } from 'fast-json-patch'; + import { Item } from '../shared/item.model'; import { AddDependentsObjectCacheAction, @@ -11,7 +12,6 @@ import { RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, } from './object-cache.actions'; - import { objectCacheReducer } from './object-cache.reducer'; class NullAction extends RemoveFromObjectCacheAction { @@ -54,7 +54,7 @@ describe('objectCacheReducer', () => { type: Item.type, self: selfLink2, foo: 'baz', - _links: { self: { href: selfLink2 } } + _links: { self: { href: selfLink2 } }, }, alternativeLinks: [altLink3, altLink4], timeCompleted: new Date().getTime(), @@ -62,8 +62,8 @@ describe('objectCacheReducer', () => { requestUUIDs: [requestUUID2], dependentRequestUUIDs: [requestUUID1], patches: [], - isDirty: false - } + isDirty: false, + }, }; deepFreeze(testState); @@ -126,6 +126,10 @@ describe('objectCacheReducer', () => { deepFreeze(state); objectCacheReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should remove the specified object from the cache in response to the REMOVE action', () => { @@ -149,6 +153,10 @@ describe('objectCacheReducer', () => { const action = new RemoveFromObjectCacheAction(selfLink1); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action', () => { @@ -164,16 +172,24 @@ describe('objectCacheReducer', () => { const action = new ResetObjectCacheTimestampsAction(new Date().getTime()); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_PATCH action without affecting the previous state', () => { const action = new AddPatchObjectCacheAction(selfLink1, [{ op: 'replace', path: '/name', - value: 'random string' + value: 'random string', }]); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should when the ADD_PATCH action dispatched', () => { diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index dc3f50db68f..631ecd22097 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,18 +1,26 @@ /* eslint-disable max-classes-per-file */ +import { + applyPatch, + Operation, +} from 'fast-json-patch'; + +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { CacheEntry } from './cache-entry'; +import { CacheableObject } from './cacheable-object.model'; import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, ObjectCacheAction, - ObjectCacheActionTypes, RemoveDependentsObjectCacheAction, + ObjectCacheActionTypes, + RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, } from './object-cache.actions'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { CacheEntry } from './cache-entry'; -import { applyPatch, Operation } from 'fast-json-patch'; -import { CacheableObject } from './cacheable-object.model'; /** * An interface to represent a JsonPatch @@ -177,8 +185,8 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio dependentRequestUUIDs: existing.dependentRequestUUIDs || [], isDirty: isNotEmpty(existing.patches), patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] - } as ObjectCacheEntry + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], + } as ObjectCacheEntry, }); } @@ -217,7 +225,7 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject const newState = Object.create(null); Object.keys(state).forEach((key) => { newState[key] = Object.assign({}, state[key], { - timeCompleted: action.payload + timeCompleted: action.payload, }); }); return newState; @@ -241,7 +249,7 @@ function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCach const patches = newState[uuid].patches; newState[uuid] = Object.assign({}, newState[uuid], { patches: [...patches, { operations } as Patch], - isDirty: true + isDirty: true, }); } return newState; @@ -286,8 +294,8 @@ function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDepen ...new Set([ ...newState[href]?.dependentRequestUUIDs || [], ...action.payload.dependentRequestUUIDs, - ]) - ] + ]), + ], }); } @@ -308,7 +316,7 @@ function removeDependentsObjectCacheState(state: ObjectCacheState, action: Remov if (hasValue(newState[href])) { newState[href] = Object.assign({}, newState[href], { - dependentRequestUUIDs: [] + dependentRequestUUIDs: [], }); } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 6af797be299..3d27f7252c5 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -1,33 +1,44 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { cold } from 'jasmine-marbles'; -import { Store, StoreModule } from '@ngrx/store'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; import { Operation } from 'fast-json-patch'; -import { empty, of as observableOf } from 'rxjs'; +import { cold } from 'jasmine-marbles'; +import { TestColdObservable } from 'jasmine-marbles/src/test-observables'; +import { + empty, + of as observableOf, +} from 'rxjs'; import { first } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; -import { coreReducers} from '../core.reducers'; +import { storeModuleConfig } from '../../app.reducer'; +import { coreReducers } from '../core.reducers'; +import { CoreState } from '../core-state.model'; import { RestRequestMethod } from '../data/rest-request-method'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { IndexName } from '../index/index-name.model'; +import { HALLink } from '../shared/hal-link.model'; import { Item } from '../shared/item.model'; import { AddDependentsObjectCacheAction, - RemoveDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, + RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction, } from './object-cache.actions'; import { Patch } from './object-cache.reducer'; import { ObjectCacheService } from './object-cache.service'; import { AddToSSBAction } from './server-sync-buffer.actions'; -import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { HALLink } from '../shared/hal-link.model'; -import { storeModuleConfig } from '../../app.reducer'; -import { TestColdObservable } from 'jasmine-marbles/src/test-observables'; -import { IndexName } from '../index/index-name.model'; -import { CoreState } from '../core-state.model'; -import { TestScheduler } from 'rxjs/testing'; describe('ObjectCacheService', () => { let service: ObjectCacheService; @@ -69,8 +80,8 @@ describe('ObjectCacheService', () => { type: Item.type, _links: { self: { href: selfLink }, - anotherLink: { href: anotherLink } - } + anotherLink: { href: anotherLink }, + }, }; cacheEntry = { data: objectToCache, @@ -96,8 +107,8 @@ describe('ObjectCacheService', () => { 'cache/syncbuffer': {}, 'cache/object-updates': {}, 'data/request': {}, - 'index': {} - } + 'index': {}, + }, }; } @@ -105,12 +116,12 @@ describe('ObjectCacheService', () => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot(coreReducers, storeModuleConfig) + StoreModule.forRoot(coreReducers, storeModuleConfig), ], providers: [ provideMockStore({ initialState }), - { provide: ObjectCacheService, useValue: service } - ] + { provide: ObjectCacheService, useValue: service }, + ], }).compileComponents(); })); @@ -120,7 +131,7 @@ describe('ObjectCacheService', () => { mockStore = store as MockStore; mockStore.setState(initialState); linkServiceStub = { - removeResolvedLinks: (a) => a + removeResolvedLinks: (a) => a, }; spyOn(linkServiceStub, 'removeResolvedLinks').and.callThrough(); spyOn(store, 'dispatch'); @@ -209,7 +220,7 @@ describe('ObjectCacheService', () => { describe('getList', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => { const item = Object.assign(new Item(), { - _links: { self: { href: selfLink } } + _links: { self: { href: selfLink } }, }); spyOn(service, 'getObjectByHref').and.returnValue(observableOf(item)); @@ -251,7 +262,7 @@ describe('ObjectCacheService', () => { 'something', 'something-else', 'specific-request', - ] + ], }))); }); @@ -266,7 +277,7 @@ describe('ObjectCacheService', () => { requestUUIDs: [ 'something', 'something-else', - ] + ], }))); }); @@ -292,9 +303,9 @@ describe('ObjectCacheService', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'cache/object': { - [selfLink]: cacheEntry - } - }) + [selfLink]: cacheEntry, + }, + }), }); mockStore.setState(state); const expected: TestColdObservable = cold('a', { a: cacheEntry }); @@ -310,14 +321,14 @@ describe('ObjectCacheService', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'cache/object': { - [selfLink]: cacheEntry + [selfLink]: cacheEntry, }, 'index': { 'object/alt-link-to-self-link': { - [anotherLink]: selfLink - } - } - }) + [anotherLink]: selfLink, + }, + }, + }), }); mockStore.setState(state); (service as any).getByAlternativeLink(anotherLink).subscribe(); @@ -335,8 +346,8 @@ describe('ObjectCacheService', () => { it('isDirty should return true when the patches list in the cache entry is not empty', () => { cacheEntry.patches = [ { - operations: operations - } as Patch + operations: operations, + } as Patch, ]; const result = (service as any).isDirty(cacheEntry); expect(result).toBe(true); @@ -371,9 +382,9 @@ describe('ObjectCacheService', () => { [anotherLink]: selfLink, ['objectWithoutDependentsAlt']: 'objectWithoutDependents', ['objectWithDependentsAlt']: 'objectWithDependents', - } - } - }) + }, + }, + }), }); mockStore.setState(state); }); @@ -421,11 +432,11 @@ describe('ObjectCacheService', () => { testScheduler.run(({ cold: tsCold, flush }) => { const href$ = tsCold('--y-n-n', { y: selfLink, - n: 'NOPE' + n: 'NOPE', }); const dependsOnHref$ = tsCold('-y-n-n', { y: 'objectWithoutDependents', - n: 'NOPE' + n: 'NOPE', }); service.addDependency(href$, dependsOnHref$); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9ca02162108..69242732a50 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,32 +1,68 @@ import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { applyPatch, Operation } from 'fast-json-patch'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; - -import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { CoreState } from '../core-state.model'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { + applyPatch, + Operation, +} from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + mergeMap, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; import { RestRequestMethod } from '../data/rest-request-method'; -import { selfLinkFromAlternativeLinkSelector, selfLinkFromUuidSelector } from '../index/index.selectors'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { + selfLinkFromAlternativeLinkSelector, + selfLinkFromUuidSelector, +} from '../index/index.selectors'; +import { IndexName } from '../index/index-name.model'; import { GenericConstructor } from '../shared/generic-constructor'; +import { HALLink } from '../shared/hal-link.model'; import { getClassForType } from './builders/build-decorators'; import { LinkService } from './builders/link.service'; -import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; - -import { ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; -import { AddToSSBAction } from './server-sync-buffer.actions'; -import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { HALLink } from '../shared/hal-link.model'; import { CacheableObject } from './cacheable-object.model'; -import { IndexName } from '../index/index-name.model'; +import { + AddDependentsObjectCacheAction, + AddPatchObjectCacheAction, + AddToObjectCacheAction, + ApplyPatchObjectCacheAction, + RemoveDependentsObjectCacheAction, + RemoveFromObjectCacheAction, +} from './object-cache.actions'; +import { + ObjectCacheEntry, + ObjectCacheState, +} from './object-cache.reducer'; +import { AddToSSBAction } from './server-sync-buffer.actions'; /** * The base selector function to select the object cache in the store */ const objectCacheSelector = createSelector( coreSelector, - (state: CoreState) => state['cache/object'] + (state: CoreState) => state['cache/object'], ); /** @@ -42,11 +78,11 @@ const entryFromSelfLinkSelector = /** * A service to interact with the object cache */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ObjectCacheService { constructor( private store: Store, - private linkService: LinkService + private linkService: LinkService, ) { } @@ -82,12 +118,12 @@ export class ObjectCacheService { const cacheEntry$ = this.getByHref(href); const altLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => entry.alternativeLinks), take(1)); const childLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => { - return Object - .entries(entry.data._links) - .filter(([key, value]: [string, HALLink]) => key !== 'self') - .map(([key, value]: [string, HALLink]) => value.href); - }), - take(1) + return Object + .entries(entry.data._links) + .filter(([key, value]: [string, HALLink]) => key !== 'self') + .map(([key, value]: [string, HALLink]) => value.href); + }), + take(1), ); this.removeLinksFromAlternativeLinkIndex(altLinks$); this.removeLinksFromAlternativeLinkIndex(childLinks$); @@ -96,8 +132,8 @@ export class ObjectCacheService { private removeLinksFromAlternativeLinkIndex(links$: Observable) { links$.subscribe((links: string[]) => links.forEach((link: string) => { - this.store.dispatch(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)); - } + this.store.dispatch(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)); + }, )); } @@ -113,8 +149,8 @@ export class ObjectCacheService { Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getObjectByHref(selfLink) - ) + mergeMap((selfLink: string) => this.getObjectByHref(selfLink), + ), ); } @@ -129,14 +165,14 @@ export class ObjectCacheService { getObjectByHref(href: string): Observable { return this.getByHref(href).pipe( map((entry: ObjectCacheEntry) => { - if (isNotEmpty(entry.patches)) { - const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); - const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument; - return Object.assign({}, entry, { data: patchedData }); - } else { - return entry; - } + if (isNotEmpty(entry.patches)) { + const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); + const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument; + return Object.assign({}, entry, { data: patchedData }); + } else { + return entry; } + }, ), map((entry: ObjectCacheEntry) => { const type: GenericConstructor = getClassForType((entry.data as any).type); @@ -144,7 +180,7 @@ export class ObjectCacheService { throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); } return Object.assign(new type(), entry.data) as T; - }) + }), ); } @@ -162,13 +198,13 @@ export class ObjectCacheService { this.getBySelfLink(href), ]).pipe( map((results: ObjectCacheEntry[]) => results.find((entry: ObjectCacheEntry) => hasValue(entry))), - filter((entry: ObjectCacheEntry) => hasValue(entry)) + filter((entry: ObjectCacheEntry) => hasValue(entry)), ); } private getBySelfLink(selfLink: string): Observable { return this.store.pipe( - select(entryFromSelfLinkSelector(selfLink)) + select(entryFromSelfLinkSelector(selfLink)), ); } @@ -204,7 +240,7 @@ export class ObjectCacheService { getRequestUUIDByObjectUUID(uuid: string): Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink)) + mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink)), ); } @@ -232,7 +268,7 @@ export class ObjectCacheService { return observableOf([]); } else { return observableCombineLatest( - selfLinks.map((selfLink: string) => this.getObjectByHref(selfLink)) + selfLinks.map((selfLink: string) => this.getObjectByHref(selfLink)), ); } } @@ -252,7 +288,7 @@ export class ObjectCacheService { /* NB: that this is only a solution because the select method is synchronous, see: https://github.com/ngrx/store/issues/296#issuecomment-269032571*/ this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - take(1) + take(1), ).subscribe((selfLink: string) => result = this.hasByHref(selfLink)); return result; @@ -290,9 +326,9 @@ export class ObjectCacheService { hasByHref$(href: string): Observable { return observableCombineLatest( this.getBySelfLink(href), - this.getByAlternativeLink(href) + this.getByAlternativeLink(href), ).pipe( - map((entries: ObjectCacheEntry[]) => entries.some((entry) => hasValue(entry))) + map((entries: ObjectCacheEntry[]) => entries.some((entry) => hasValue(entry))), ); } @@ -358,7 +394,7 @@ export class ObjectCacheService { observableCombineLatest([ href$, dependsOnHref$.pipe( - switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref)) + switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref)), ), ]).pipe( switchMap(([href, dependsOnSelfLink]: [string, string]) => { @@ -373,7 +409,7 @@ export class ObjectCacheService { this.getByHref(href).pipe( // only add the latest request to keep dependency index from growing indefinitely map((entry: ObjectCacheEntry) => entry?.requestUUIDs?.[0]), - ) + ), ]); }), take(1), diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 197bf130fb2..9a09a49bc81 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -1,10 +1,10 @@ /* eslint-disable max-classes-per-file */ -import { PageInfo } from '../shared/page-info.model'; import { ConfigObject } from '../config/models/config.model'; +import { RequestError } from '../data/request-error.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALLink } from '../shared/hal-link.model'; +import { PageInfo } from '../shared/page-info.model'; import { UnCacheableObject } from '../shared/uncacheable-object.model'; -import { RequestError } from '../data/request-error.model'; export class RestResponse { public toCache = true; @@ -13,7 +13,7 @@ export class RestResponse { constructor( public isSuccessful: boolean, public statusCode: number, - public statusText: string + public statusText: string, ) { } } @@ -29,7 +29,7 @@ export class DSOSuccessResponse extends RestResponse { public resourceSelfLinks: string[], public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -43,7 +43,7 @@ export class EndpointMapSuccessResponse extends RestResponse { constructor( public endpointMap: EndpointMap, public statusCode: number, - public statusText: string + public statusText: string, ) { super(true, statusCode, statusText); } @@ -64,7 +64,7 @@ export class ConfigSuccessResponse extends RestResponse { public configDefinition: ConfigObject, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -78,7 +78,7 @@ export class TokenResponse extends RestResponse { public token: string, public isSuccessful: boolean, public statusCode: number, - public statusText: string + public statusText: string, ) { super(isSuccessful, statusCode, statusText); } @@ -89,7 +89,7 @@ export class PostPatchSuccessResponse extends RestResponse { public dataDefinition: any, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -100,7 +100,7 @@ export class EpersonSuccessResponse extends RestResponse { public epersonDefinition: DSpaceObject[], public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -112,7 +112,7 @@ export class MessageResponse extends RestResponse { constructor( public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -124,7 +124,7 @@ export class TaskResponse extends RestResponse { constructor( public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -135,7 +135,7 @@ export class FilteredDiscoveryQueryResponse extends RestResponse { public filterQuery: string, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index 833c6b580fd..889b3b7454a 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -1,12 +1,22 @@ import { TestBed } from '@angular/core/testing'; - import { provideMockActions } from '@ngrx/effects/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { cold, hot } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import { storeModuleConfig } from '../../app.reducer'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; import { StoreMock } from '../../shared/testing/store.mock'; import { RequestService } from '../data/request.service'; import { RestRequestMethod } from '../data/rest-request-method'; @@ -16,11 +26,9 @@ import { ObjectCacheService } from './object-cache.service'; import { CommitSSBAction, EmptySSBAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; -import { storeModuleConfig } from '../../app.reducer'; -import { NoOpAction } from '../../shared/ngrx/no-op.action'; describe('ServerSyncBufferEffects', () => { let ssbEffects: ServerSyncBufferEffects; @@ -32,9 +40,9 @@ describe('ServerSyncBufferEffects', () => { autoSync: { timePerMethod: {}, - defaultTime: 0 - } - } + defaultTime: 0, + }, + }, }; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; let store; @@ -52,21 +60,21 @@ describe('ServerSyncBufferEffects', () => { provide: ObjectCacheService, useValue: { getObjectBySelfLink: (link) => { const object = Object.assign(new DSpaceObject(), { - _links: { self: { href: link } } + _links: { self: { href: link } }, }); return observableOf(object); }, getByHref: (link) => { const object = Object.assign(new DSpaceObject(), { _links: { - self: { href: link } - } + self: { href: link }, + }, }); return observableOf(object); - } - } + }, + }, }, - { provide: Store, useClass: StoreMock } + { provide: Store, useClass: StoreMock }, // other providers ], }); @@ -88,12 +96,12 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.ADD, - payload: { href: selfLink, method: RestRequestMethod.PUT } - } + payload: { href: selfLink, method: RestRequestMethod.PUT }, + }, }); expectObservable(ssbEffects.setTimeoutForServerSync).toBe('b', { - b: new CommitSSBAction(RestRequestMethod.PUT) + b: new CommitSSBAction(RestRequestMethod.PUT), }); }); }); @@ -108,8 +116,8 @@ describe('ServerSyncBufferEffects', () => { (state as any).core['cache/syncbuffer'] = { buffer: [{ href: selfLink, - method: RestRequestMethod.PATCH - }] + method: RestRequestMethod.PATCH, + }], }; }); }); @@ -117,13 +125,13 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.COMMIT, - payload: RestRequestMethod.PATCH - } + payload: RestRequestMethod.PATCH, + }, }); const expected = cold('(bc)', { b: new ApplyPatchObjectCacheAction(selfLink), - c: new EmptySSBAction(RestRequestMethod.PATCH) + c: new EmptySSBAction(RestRequestMethod.PATCH), }); expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected); @@ -136,7 +144,7 @@ describe('ServerSyncBufferEffects', () => { .subscribe((state) => { (state as any).core = Object({}); (state as any).core['cache/syncbuffer'] = { - buffer: [] + buffer: [], }; }); }); @@ -145,8 +153,8 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.COMMIT, - payload: { method: RestRequestMethod.PATCH } - } + payload: { method: RestRequestMethod.PATCH }, + }, }); const expected = cold('b', { b: new NoOpAction() }); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 9571d4af5b4..6f346d5bb3f 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,27 +1,55 @@ -import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { + Action, + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + delay, + exhaustMap, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; +import { + hasValue, + isNotEmpty, + isNotUndefined, +} from '../../shared/empty.util'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; +import { PatchRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { ApplyPatchObjectCacheAction } from './object-cache.actions'; +import { ObjectCacheEntry } from './object-cache.reducer'; +import { ObjectCacheService } from './object-cache.service'; import { AddToSSBAction, CommitSSBAction, EmptySSBAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; -import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { RequestService } from '../data/request.service'; -import { PatchRequest } from '../data/request.models'; -import { ObjectCacheService } from './object-cache.service'; -import { ApplyPatchObjectCacheAction } from './object-cache.actions'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { environment } from '../../../environments/environment'; -import { ObjectCacheEntry } from './object-cache.reducer'; -import { Operation } from 'fast-json-patch'; -import { NoOpAction } from '../../shared/ngrx/no-op.action'; -import { CoreState } from '../core-state.model'; +import { + ServerSyncBufferEntry, + ServerSyncBufferState, +} from './server-sync-buffer.reducer'; @Injectable() export class ServerSyncBufferEffects { @@ -32,7 +60,7 @@ export class ServerSyncBufferEffects { * Then dispatch a CommitSSBAction * When the delay is running, no new AddToSSBActions are processed in this effect */ - setTimeoutForServerSync = createEffect(() => this.actions$ + setTimeoutForServerSync = createEffect(() => this.actions$ .pipe( ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { @@ -41,7 +69,7 @@ export class ServerSyncBufferEffects { return observableOf(new CommitSSBAction(action.payload.method)).pipe( delay(timeoutInSeconds * 1000), ); - }) + }), )); /** @@ -50,7 +78,7 @@ export class ServerSyncBufferEffects { * When the list of actions is not empty, also dispatch an EmptySSBAction * When the list is empty dispatch a NO_ACTION placeholder action */ - commitServerSyncBuffer = createEffect(() => this.actions$ + commitServerSyncBuffer = createEffect(() => this.actions$ .pipe( ofType(ServerSyncBufferActionTypes.COMMIT), switchMap((action: CommitSSBAction) => { @@ -78,14 +106,14 @@ export class ServerSyncBufferEffects { /* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */ if (isNotEmpty(actions) && isNotUndefined(actions[0])) { return observableCombineLatest(...actions).pipe( - switchMap((array) => [...array, new EmptySSBAction(action.payload)]) - ); + switchMap((array) => [...array, new EmptySSBAction(action.payload)]), + ); } else { return observableOf(new NoOpAction()); } - }) + }), ); - }) + }), )); /** @@ -96,7 +124,7 @@ export class ServerSyncBufferEffects { */ private applyPatch(href: string): Observable { const patchObject = this.objectCache.getByHref(href).pipe( - take(1) + take(1), ); return patchObject.pipe( @@ -108,7 +136,7 @@ export class ServerSyncBufferEffects { } } return new ApplyPatchObjectCacheAction(href); - }) + }), ); } diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts index 51ba010c1e3..d986581ce2e 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.spec.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts @@ -1,9 +1,13 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; + +import { RestRequestMethod } from '../data/rest-request-method'; import { RemoveFromObjectCacheAction } from './object-cache.actions'; +import { + AddToSSBAction, + EmptySSBAction, +} from './server-sync-buffer.actions'; import { serverSyncBufferReducer } from './server-sync-buffer.reducer'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { AddToSSBAction, EmptySSBAction } from './server-sync-buffer.actions'; class NullAction extends RemoveFromObjectCacheAction { type = null; @@ -27,8 +31,8 @@ describe('serverSyncBufferReducer', () => { { href: selfLink2, method: RestRequestMethod.GET, - } - ] + }, + ], }; const newSelfLink = 'https://localhost:8080/api/core/items/1ce6b5ae-97e1-4e5a-b4b0-f9029bad10c0'; @@ -52,12 +56,20 @@ describe('serverSyncBufferReducer', () => { const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the EMPTY action without affecting the previous state', () => { const action = new EmptySSBAction(); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => { @@ -79,7 +91,7 @@ describe('serverSyncBufferReducer', () => { // testState has already been frozen above const newState = serverSyncBufferReducer(testState, action); expect(newState.buffer).toContain({ - href: newSelfLink, method: RestRequestMethod.PUT + href: newSelfLink, method: RestRequestMethod.PUT, }) ; }); diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index 3e8944aa731..f1ae8943151 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -1,11 +1,14 @@ -import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { + hasNoValue, + hasValue, +} from '../../shared/empty.util'; +import { RestRequestMethod } from '../data/rest-request-method'; import { AddToSSBAction, EmptySSBAction, ServerSyncBufferAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; -import { RestRequestMethod } from '../data/rest-request-method'; /** * An entry in the ServerSyncBufferState @@ -86,9 +89,9 @@ function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBActi * the new state, with a new entry added to the buffer */ function emptyServerSyncQueue(state: ServerSyncBufferState, action: EmptySSBAction): ServerSyncBufferState { - let newBuffer = []; - if (hasValue(action.payload)) { - newBuffer = state.buffer.filter((entry) => entry.method !== action.payload); - } - return Object.assign({}, state, { buffer: newBuffer }); + let newBuffer = []; + if (hasValue(action.payload)) { + newBuffer = state.buffer.filter((entry) => entry.method !== action.payload); + } + return Object.assign({}, state, { buffer: newBuffer }); } diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts index 81ac0db8d81..706c8f684b9 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts @@ -1,36 +1,27 @@ -import { TestBed } from '@angular/core/testing'; - -import { NotifyInfoGuard } from './notify-info.guard'; -import { Router } from '@angular/router'; -import { NotifyInfoService } from './notify-info.service'; import { of } from 'rxjs'; -describe('NotifyInfoGuard', () => { - let guard: NotifyInfoGuard; +import { notifyInfoGuard } from './notify-info.guard'; + +describe('notifyInfoGuard', () => { + let guard: any; let notifyInfoServiceSpy: any; let router: any; beforeEach(() => { notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); router = jasmine.createSpyObj('Router', ['parseUrl']); - TestBed.configureTestingModule({ - providers: [ - NotifyInfoGuard, - { provide: NotifyInfoService, useValue: notifyInfoServiceSpy}, - { provide: Router, useValue: router} - ] - }); - guard = TestBed.inject(NotifyInfoGuard); + guard = notifyInfoGuard; }); it('should be created', () => { - expect(guard).toBeTruthy(); + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); + expect(guard(null, null, notifyInfoServiceSpy, router)).toBeTruthy(); }); it('should return true if COAR config is enabled', (done) => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); - guard.canActivate(null, null).subscribe((result) => { + guard(null, null, notifyInfoServiceSpy, router).subscribe((result) => { expect(result).toBe(true); done(); }); @@ -40,7 +31,7 @@ describe('NotifyInfoGuard', () => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); router.parseUrl.and.returnValue(of('/404')); - guard.canActivate(null, null).subscribe(() => { + guard(null, null, notifyInfoServiceSpy, router).subscribe(() => { expect(router.parseUrl).toHaveBeenCalledWith('/404'); done(); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.ts index 7af08216184..1025e7b62bc 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.ts @@ -1,30 +1,23 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { NotifyInfoService } from './notify-info.service'; import { map } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) -export class NotifyInfoGuard implements CanActivate { - constructor( - private notifyInfoService: NotifyInfoService, - private router: Router - ) {} +import { NotifyInfoService } from './notify-info.service'; - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable { - return this.notifyInfoService.isCoarConfigEnabled().pipe( - map(coarLdnEnabled => { - if (coarLdnEnabled) { - return true; - } else { - return this.router.parseUrl('/404'); - } - }) - ); - } -} +export const notifyInfoGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + notifyInfoService: NotifyInfoService = inject(NotifyInfoService), + router: Router = inject(Router), +): Observable => { + return notifyInfoService.isCoarConfigEnabled().pipe( + map(isEnabled => isEnabled ? true : router.parseUrl('/404')), + ); +}; diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts index a3cc360a969..6fa8295be06 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts @@ -1,27 +1,31 @@ import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; -import { NotifyInfoService } from './notify-info.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { ConfigurationDataService } from '../../data/configuration-data.service'; -import { of } from 'rxjs'; import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service'; +import { NotifyInfoService } from './notify-info.service'; describe('NotifyInfoService', () => { let service: NotifyInfoService; let configurationDataService: any; let authorizationDataService: any; - beforeEach(() => { - authorizationDataService = { - isAuthorized: jasmine.createSpy('isAuthorized').and.returnValue(of(true)), - }; - configurationDataService = { - findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(of({})), - }; + beforeEach(() => { + authorizationDataService = { + isAuthorized: jasmine.createSpy('isAuthorized').and.returnValue(of(true)), + }; + configurationDataService = { + findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(of({})), + }; TestBed.configureTestingModule({ providers: [ NotifyInfoService, { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: AuthorizationDataService, useValue: authorizationDataService } - ] + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + ], }); service = TestBed.inject(NotifyInfoService); authorizationDataService = TestBed.inject(AuthorizationDataService); @@ -32,21 +36,21 @@ describe('NotifyInfoService', () => { expect(service).toBeTruthy(); }); - it('should retrieve and map coar configuration', () => { - const mockResponse = { payload: { values: ['true'] } }; - (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + it('should retrieve and map coar configuration', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['true'] })); service.isCoarConfigEnabled().subscribe((result) => { expect(result).toBe(true); + done(); }); }); - it('should retrieve and map LDN local inbox URLs', () => { - const mockResponse = { values: ['inbox1', 'inbox2'] }; - (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(of(mockResponse)); + it('should retrieve and map LDN local inbox URLs', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['inbox1', 'inbox2'] })); service.getCoarLdnLocalInboxUrls().subscribe((result) => { expect(result).toEqual(['inbox1', 'inbox2']); + done(); }); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.ts b/src/app/core/coar-notify/notify-info/notify-info.service.ts index a15c64237ce..455c7902ee4 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.ts @@ -1,52 +1,54 @@ import { Injectable } from '@angular/core'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators'; +import { + map, + Observable, +} from 'rxjs'; + import { ConfigurationDataService } from '../../data/configuration-data.service'; -import { map, Observable } from 'rxjs'; -import { ConfigurationProperty } from '../../shared/configuration-property.model'; import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../data/feature-authorization/feature-id'; +import { RemoteData } from '../../data/remote-data'; +import { ConfigurationProperty } from '../../shared/configuration-property.model'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; /** * Service to check COAR availability and LDN services information for the COAR Notify functionalities */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class NotifyInfoService { - /** + /** * The relation link for the inbox */ - private _inboxRelationLink = 'http://www.w3.org/ns/ldp#inbox'; + private _inboxRelationLink = 'http://www.w3.org/ns/ldp#inbox'; - constructor( + constructor( private configService: ConfigurationDataService, protected authorizationService: AuthorizationDataService, - ) {} + ) {} - isCoarConfigEnabled(): Observable { - return this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled); - } + isCoarConfigEnabled(): Observable { + return this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled); + } - /** + /** * Get the url of the local inbox from the REST configuration * @returns the url of the local inbox */ - getCoarLdnLocalInboxUrls(): Observable { - return this.configService.findByPropertyName('ldn.notify.inbox').pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((response: ConfigurationProperty) => { - return response.values; - }) - ); - } + getCoarLdnLocalInboxUrls(): Observable { + return this.configService.findByPropertyName('ldn.notify.inbox').pipe( + getFirstCompletedRemoteData(), + map((responseRD: RemoteData) => responseRD.hasSucceeded ? responseRD.payload.values : []), + ); + } - /** + /** * Method to get the relation link for the inbox * @returns the relation link for the inbox */ - getInboxRelationLink(): string { - return this._inboxRelationLink; - } + getInboxRelationLink(): string { + return this._inboxRelationLink; + } } diff --git a/src/app/core/config/bulk-access-config-data.service.ts b/src/app/core/config/bulk-access-config-data.service.ts index 28b4029ea28..8023e58489b 100644 --- a/src/app/core/config/bulk-access-config-data.service.ts +++ b/src/app/core/config/bulk-access-config-data.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@angular/core'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; -import { dataService } from '../data/base/data-service.decorator'; -import { BULK_ACCESS_CONDITION_OPTIONS } from './models/config-type'; /** * Data Service responsible for retrieving Bulk Access Condition Options from the REST API */ @Injectable({ providedIn: 'root' }) -@dataService(BULK_ACCESS_CONDITION_OPTIONS) export class BulkAccessConfigDataService extends ConfigDataService { constructor( diff --git a/src/app/core/config/config-data.service.spec.ts b/src/app/core/config/config-data.service.spec.ts index 38340d1ad54..a9979f1bb5b 100644 --- a/src/app/core/config/config-data.service.spec.ts +++ b/src/app/core/config/config-data.service.spec.ts @@ -1,15 +1,16 @@ import { getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { ConfigDataService } from './config-data.service'; -import { RequestService } from '../data/request.service'; -import { GetRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { FindListOptions } from '../data/find-list-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { FindListOptions } from '../data/find-list-options.model'; +import { GetRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigDataService } from './config-data.service'; const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; diff --git a/src/app/core/config/config-data.service.ts b/src/app/core/config/config-data.service.ts index 58b023e62c8..51ca6559219 100644 --- a/src/app/core/config/config-data.service.ts +++ b/src/app/core/config/config-data.service.ts @@ -1,11 +1,11 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ConfigObject } from './models/config.model'; -import { RemoteData } from '../data/remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { RemoteData } from '../data/remote-data'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { ConfigObject } from './models/config.model'; /** * Abstract data service to retrieve configuration objects from the REST server. diff --git a/src/app/core/config/models/bulk-access-condition-options.model.ts b/src/app/core/config/models/bulk-access-condition-options.model.ts index d84e14b95db..c4913438524 100644 --- a/src/app/core/config/models/bulk-access-condition-options.model.ts +++ b/src/app/core/config/models/bulk-access-condition-options.model.ts @@ -1,8 +1,13 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { + autoserialize, + autoserializeAs, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { ResourceType } from '../../shared/resource-type'; import { HALLink } from '../../shared/hal-link.model'; +import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; import { ConfigObject } from './config.model'; import { AccessesConditionOption } from './config-accesses-conditions-options.model'; import { BULK_ACCESS_CONDITION_OPTIONS } from './config-type'; @@ -20,19 +25,19 @@ export class BulkAccessConditionOptions extends ConfigObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserializeAs(String, 'name') - uuid: string; + uuid: string; @autoserialize - id: string; + id: string; @autoserialize - itemAccessConditionOptions: AccessesConditionOption[]; + itemAccessConditionOptions: AccessesConditionOption[]; @autoserialize - bitstreamAccessConditionOptions: AccessesConditionOption[]; + bitstreamAccessConditionOptions: AccessesConditionOption[]; _links: { self: HALLink }; } diff --git a/src/app/core/config/models/config-accesses-conditions-options.model.ts b/src/app/core/config/models/config-accesses-conditions-options.model.ts index 244b5019086..64199be0eb7 100644 --- a/src/app/core/config/models/config-accesses-conditions-options.model.ts +++ b/src/app/core/config/models/config-accesses-conditions-options.model.ts @@ -3,43 +3,43 @@ */ export class AccessesConditionOption { - /** + /** * The name for this Access Condition */ - name: string; + name: string; - /** + /** * The groupName for this Access Condition */ - groupName: string; + groupName: string; - /** + /** * A boolean representing if this Access Condition has a start date */ - hasStartDate: boolean; + hasStartDate: boolean; - /** + /** * A boolean representing if this Access Condition has an end date */ - hasEndDate: boolean; + hasEndDate: boolean; - /** + /** * Maximum value of the start date */ - endDateLimit?: string; + endDateLimit?: string; - /** + /** * Maximum value of the end date */ - startDateLimit?: string; + startDateLimit?: string; - /** + /** * Maximum value of the start date */ - maxStartDate?: string; + maxStartDate?: string; - /** + /** * Maximum value of the end date */ - maxEndDate?: string; + maxEndDate?: string; } diff --git a/src/app/core/config/models/config-submission-access.model.ts b/src/app/core/config/models/config-submission-access.model.ts index 7db96acf2bd..4617dc47191 100644 --- a/src/app/core/config/models/config-submission-access.model.ts +++ b/src/app/core/config/models/config-submission-access.model.ts @@ -1,9 +1,14 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; +import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { AccessesConditionOption } from './config-accesses-conditions-options.model'; import { SUBMISSION_ACCESSES_TYPE } from './config-type'; -import { HALLink } from '../../shared/hal-link.model'; /** * Class for the configuration describing the item accesses condition @@ -17,25 +22,25 @@ export class SubmissionAccessModel extends ConfigObject { * A list of available item access conditions */ @autoserialize - accessConditionOptions: AccessesConditionOption[]; + accessConditionOptions: AccessesConditionOption[]; /** * Boolean that indicates whether the current item must be findable via search or browse. */ @autoserialize - discoverable: boolean; + discoverable: boolean; /** * Boolean that indicates whether or not the user can change the discoverable flag. */ @autoserialize - canChangeDiscoverable: boolean; + canChangeDiscoverable: boolean; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink }; diff --git a/src/app/core/config/models/config-submission-accesses.model.ts b/src/app/core/config/models/config-submission-accesses.model.ts index 3f8004928db..b3c097cc8ac 100644 --- a/src/app/core/config/models/config-submission-accesses.model.ts +++ b/src/app/core/config/models/config-submission-accesses.model.ts @@ -1,7 +1,8 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { SUBMISSION_ACCESSES_TYPE } from './config-type'; import { SubmissionAccessModel } from './config-submission-access.model'; +import { SUBMISSION_ACCESSES_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionAccessModel) diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts index b07917e0328..2d6b1ad604c 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -1,9 +1,14 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { PaginatedList } from '../../data/paginated-list.model'; import { HALLink } from '../../shared/hal-link.model'; -import { SubmissionSectionModel } from './config-submission-section.model'; import { ConfigObject } from './config.model'; +import { SubmissionSectionModel } from './config-submission-section.model'; import { SUBMISSION_DEFINITION_TYPE } from './config-type'; /** @@ -18,20 +23,20 @@ export class SubmissionDefinitionModel extends ConfigObject { * A boolean representing if this submission definition is the default or not */ @autoserialize - isDefault: boolean; + isDefault: boolean; /** * A list of SubmissionSectionModel that are present in this submission definition */ // TODO refactor using remotedata @deserialize - sections: PaginatedList; + sections: PaginatedList; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, collections: HALLink, sections: HALLink diff --git a/src/app/core/config/models/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts index 08f1ef17bb0..790334da9bd 100644 --- a/src/app/core/config/models/config-submission-definitions.model.ts +++ b/src/app/core/config/models/config-submission-definitions.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionDefinitionModel } from './config-submission-definition.model'; import { SUBMISSION_DEFINITIONS_TYPE } from './config-type'; diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts index 90f94882bd6..f6011adc764 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -1,7 +1,11 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + inheritSerialization, +} from 'cerialize'; + +import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { typedObject } from '../../cache/builders/build-decorators'; import { ConfigObject } from './config.model'; -import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { SUBMISSION_FORM_TYPE } from './config-type'; /** @@ -23,5 +27,5 @@ export class SubmissionFormModel extends ConfigObject { * An array of [FormRowModel] that are present in this form */ @autoserialize - rows: FormRowModel[]; + rows: FormRowModel[]; } diff --git a/src/app/core/config/models/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts index 506905d88cb..4cf71d85d9a 100644 --- a/src/app/core/config/models/config-submission-forms.model.ts +++ b/src/app/core/config/models/config-submission-forms.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionFormModel } from './config-submission-form.model'; import { SUBMISSION_FORMS_TYPE } from './config-type'; diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index bdc884dfa46..0d4ae9aa102 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -1,4 +1,9 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { SectionsType } from '../../../submission/sections/sections-type'; import { typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; @@ -22,31 +27,31 @@ export class SubmissionSectionModel extends ConfigObject { * The header for this section */ @autoserialize - header: string; + header: string; /** * A boolean representing if this submission section is the mandatory or not */ @autoserialize - mandatory: boolean; + mandatory: boolean; /** * A string representing the kind of section object */ @autoserialize - sectionType: SectionsType; + sectionType: SectionsType; /** * The [SubmissionSectionVisibility] object for this section */ @autoserialize - visibility: SubmissionSectionVisibility; + visibility: SubmissionSectionVisibility; /** * The {@link HALLink}s for this SubmissionSectionModel */ @deserialize - _links: { + _links: { self: HALLink; config: HALLink; }; diff --git a/src/app/core/config/models/config-submission-sections.model.ts b/src/app/core/config/models/config-submission-sections.model.ts index 423ea99b1e1..86894b6e44b 100644 --- a/src/app/core/config/models/config-submission-sections.model.ts +++ b/src/app/core/config/models/config-submission-sections.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionSectionModel } from './config-submission-section.model'; import { SUBMISSION_SECTIONS_TYPE } from './config-type'; diff --git a/src/app/core/config/models/config-submission-upload.model.ts b/src/app/core/config/models/config-submission-upload.model.ts index f6897da2e38..edc4626f83e 100644 --- a/src/app/core/config/models/config-submission-upload.model.ts +++ b/src/app/core/config/models/config-submission-upload.model.ts @@ -1,12 +1,23 @@ -import { autoserialize, inheritSerialization, deserialize } from 'cerialize'; -import { typedObject, link } from '../../cache/builders/build-decorators'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; +import { Observable } from 'rxjs'; + +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; +import { RemoteData } from '../../data/remote-data'; +import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { AccessConditionOption } from './config-access-condition-option.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; -import { SUBMISSION_UPLOAD_TYPE, SUBMISSION_FORMS_TYPE } from './config-type'; -import { HALLink } from '../../shared/hal-link.model'; -import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs'; +import { + SUBMISSION_FORMS_TYPE, + SUBMISSION_UPLOAD_TYPE, +} from './config-type'; @typedObject @inheritSerialization(ConfigObject) @@ -16,22 +27,22 @@ export class SubmissionUploadModel extends ConfigObject { * A list of available bitstream access conditions */ @autoserialize - accessConditionOptions: AccessConditionOption[]; + accessConditionOptions: AccessConditionOption[]; /** * An object representing the configuration describing the bitstream metadata form */ @link(SUBMISSION_FORMS_TYPE) - metadata?: Observable>; + metadata?: Observable>; @autoserialize - required: boolean; + required: boolean; @autoserialize - maxSize: number; + maxSize: number; @deserialize - _links: { + _links: { metadata: HALLink self: HALLink }; diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts index 8fb7dc66b9c..235cdd31a1e 100644 --- a/src/app/core/config/models/config-submission-uploads.model.ts +++ b/src/app/core/config/models/config-submission-uploads.model.ts @@ -1,7 +1,8 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { SUBMISSION_UPLOADS_TYPE } from './config-type'; import { SubmissionUploadModel } from './config-submission-upload.model'; +import { SUBMISSION_UPLOADS_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionUploadModel) diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index 170aa334edb..c1db44e8919 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -1,8 +1,12 @@ -import { autoserialize, deserialize } from 'cerialize'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + +import { CacheableObject } from '../../cache/cacheable-object.model'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { CacheableObject } from '../../cache/cacheable-object.model'; export abstract class ConfigObject implements CacheableObject { @@ -23,13 +27,13 @@ export abstract class ConfigObject implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, [name: string]: HALLink }; diff --git a/src/app/core/config/submission-accesses-config-data.service.ts b/src/app/core/config/submission-accesses-config-data.service.ts index d2da0fce422..bc7a7e66d3c 100644 --- a/src/app/core/config/submission-accesses-config-data.service.ts +++ b/src/app/core/config/submission-accesses-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; +import { Observable } from 'rxjs'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SUBMISSION_ACCESSES_TYPE } from './models/config-type'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionAccessesModel } from './models/config-submission-accesses.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. */ -@Injectable() -@dataService(SUBMISSION_ACCESSES_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionAccessesConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/config/submission-forms-config-data.service.ts b/src/app/core/config/submission-forms-config-data.service.ts index f4c0690685b..fe1234defb3 100644 --- a/src/app/core/config/submission-forms-config-data.service.ts +++ b/src/app/core/config/submission-forms-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; -import { RequestService } from '../data/request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Observable } from 'rxjs'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteData } from '../data/remote-data'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; -import { SUBMISSION_FORMS_TYPE } from './models/config-type'; import { SubmissionFormsModel } from './models/config-submission-forms.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * Data service to retrieve submission form configuration objects from the REST server. */ -@Injectable() -@dataService(SUBMISSION_FORMS_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionFormsConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/config/submission-uploads-config-data.service.ts b/src/app/core/config/submission-uploads-config-data.service.ts index 8f838352a9e..10d749080e4 100644 --- a/src/app/core/config/submission-uploads-config-data.service.ts +++ b/src/app/core/config/submission-uploads-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; +import { Observable } from 'rxjs'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SUBMISSION_UPLOADS_TYPE } from './models/config-type'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionUploadsModel } from './models/config-submission-uploads.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. */ -@Injectable() -@dataService(SUBMISSION_UPLOADS_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionUploadsConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/core-state.model.ts b/src/app/core/core-state.model.ts index b8211fdb555..2128901754b 100644 --- a/src/app/core/core-state.model.ts +++ b/src/app/core/core-state.model.ts @@ -1,16 +1,14 @@ -import { - BitstreamFormatRegistryState -} from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { BitstreamFormatRegistryState } from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { AuthState } from './auth/auth.reducer'; import { ObjectCacheState } from './cache/object-cache.reducer'; import { ServerSyncBufferState } from './cache/server-sync-buffer.reducer'; import { ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; +import { RequestState } from './data/request-state.model'; import { HistoryState } from './history/history.reducer'; import { MetaIndexState } from './index/index.reducer'; -import { AuthState } from './auth/auth.reducer'; import { JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer'; import { MetaTagState } from './metadata/meta-tag.reducer'; import { RouteState } from './services/route.reducer'; -import { RequestState } from './data/request-state.model'; /** * The core sub-state in the NgRx store diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 1724e88743b..5af2fe580a1 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,13 +1,13 @@ -import { ObjectCacheEffects } from './cache/object-cache.effects'; -import { UUIDIndexEffects } from './index/index.effects'; -import { RequestEffects } from './data/request.effects'; +import { MenuEffects } from '../shared/menu/menu.effects'; import { AuthEffects } from './auth/auth.effects'; -import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; +import { ObjectCacheEffects } from './cache/object-cache.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; -import { RouteEffects } from './services/route.effects'; +import { RequestEffects } from './data/request.effects'; +import { UUIDIndexEffects } from './index/index.effects'; +import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; import { RouterEffects } from './router/router.effects'; -import { MenuEffects } from '../shared/menu/menu.effects'; +import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts deleted file mode 100644 index 7088860674a..00000000000 --- a/src/app/core/core.module.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; -import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; - -import { EffectsModule } from '@ngrx/effects'; - -import { Action, StoreConfig, StoreModule } from '@ngrx/store'; -import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard'; - -import { isNotEmpty } from '../shared/empty.util'; -import { HostWindowService } from '../shared/host-window.service'; -import { MenuService } from '../shared/menu/menu.service'; -import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service'; -import { - MOCK_RESPONSE_MAP, - mockResponseMap, - ResponseMapMock -} from '../shared/mocks/dspace-rest/mocks/response-map.mock'; -import { NotificationsService } from '../shared/notifications/notifications.service'; -import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; -import { ObjectSelectService } from '../shared/object-select/object-select.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { AuthenticatedGuard } from './auth/authenticated.guard'; -import { AuthStatus } from './auth/models/auth-status.model'; -import { BrowseService } from './browse/browse.service'; -import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; -import { ObjectCacheService } from './cache/object-cache.service'; -import { SubmissionDefinitionsModel } from './config/models/config-submission-definitions.model'; -import { SubmissionFormsModel } from './config/models/config-submission-forms.model'; -import { SubmissionSectionModel } from './config/models/config-submission-section.model'; -import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model'; -import { SubmissionFormsConfigDataService } from './config/submission-forms-config-data.service'; -import { coreEffects } from './core.effects'; -import { coreReducers } from './core.reducers'; -import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; -import { CollectionDataService } from './data/collection-data.service'; -import { CommunityDataService } from './data/community-data.service'; -import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; -import { DebugResponseParsingService } from './data/debug-response-parsing.service'; -import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; -import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; -import { DSOResponseParsingService } from './data/dso-response-parsing.service'; -import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { EntityTypeDataService } from './data/entity-type-data.service'; -import { ExternalSourceDataService } from './data/external-source-data.service'; -import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; -import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; -import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; -import { ItemDataService } from './data/item-data.service'; -import { LookupRelationService } from './data/lookup-relation.service'; -import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; -import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { RelationshipTypeDataService } from './data/relationship-type-data.service'; -import { RelationshipDataService } from './data/relationship-data.service'; -import { ResourcePolicyDataService } from './resource-policy/resource-policy-data.service'; -import { SearchResponseParsingService } from './data/search-response-parsing.service'; -import { SiteDataService } from './data/site-data.service'; -import { DspaceRestService } from './dspace-rest/dspace-rest.service'; -import { EPersonDataService } from './eperson/eperson-data.service'; -import { EPerson } from './eperson/models/eperson.model'; -import { Group } from './eperson/models/group.model'; -import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; -import { MetadataField } from './metadata/metadata-field.model'; -import { MetadataSchema } from './metadata/metadata-schema.model'; -import { MetadataService } from './metadata/metadata.service'; -import { RegistryService } from './registry/registry.service'; -import { RoleService } from './roles/role.service'; -import { FeedbackDataService } from './feedback/feedback-data.service'; - -import { ServerResponseService } from './services/server-response.service'; -import { NativeWindowFactory, NativeWindowService } from './services/window.service'; -import { BitstreamFormat } from './shared/bitstream-format.model'; -import { Bitstream } from './shared/bitstream.model'; -import { BrowseDefinition } from './shared/browse-definition.model'; -import { BrowseEntry } from './shared/browse-entry.model'; -import { Bundle } from './shared/bundle.model'; -import { Collection } from './shared/collection.model'; -import { Community } from './shared/community.model'; -import { DSpaceObject } from './shared/dspace-object.model'; -import { ExternalSourceEntry } from './shared/external-source-entry.model'; -import { ExternalSource } from './shared/external-source.model'; -import { HALEndpointService } from './shared/hal-endpoint.service'; -import { ItemType } from './shared/item-relationships/item-type.model'; -import { RelationshipType } from './shared/item-relationships/relationship-type.model'; -import { Relationship } from './shared/item-relationships/relationship.model'; -import { Item } from './shared/item.model'; -import { License } from './shared/license.model'; -import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; -import { SearchConfigurationService } from './shared/search/search-configuration.service'; -import { SearchFilterService } from './shared/search/search-filter.service'; -import { SearchService } from './shared/search/search.service'; -import { Site } from './shared/site.model'; -import { UUIDService } from './shared/uuid.service'; -import { WorkflowItem } from './submission/models/workflowitem.model'; -import { WorkspaceItem } from './submission/models/workspaceitem.model'; -import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service'; -import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; -import { SubmissionRestService } from './submission/submission-rest.service'; -import { WorkflowItemDataService } from './submission/workflowitem-data.service'; -import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; -import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; -import { ClaimedTask } from './tasks/models/claimed-task-object.model'; -import { PoolTask } from './tasks/models/pool-task-object.model'; -import { TaskObject } from './tasks/models/task-object.model'; -import { PoolTaskDataService } from './tasks/pool-task-data.service'; -import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; -import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; -import { BitstreamDataService } from './data/bitstream-data.service'; -import { environment } from '../../environments/environment'; -import { storeModuleConfig } from '../app.reducer'; -import { VersionDataService } from './data/version-data.service'; -import { VersionHistoryDataService } from './data/version-history-data.service'; -import { Version } from './shared/version.model'; -import { VersionHistory } from './shared/version-history.model'; -import { Script } from '../process-page/scripts/script.model'; -import { Process } from '../process-page/processes/process.model'; -import { ProcessDataService } from './data/processes/process-data.service'; -import { ScriptDataService } from './data/processes/script-data.service'; -import { WorkflowActionDataService } from './data/workflow-action-data.service'; -import { WorkflowAction } from './tasks/models/workflow-action-object.model'; -import { ItemTemplateDataService } from './data/item-template-data.service'; -import { TemplateItem } from './shared/template-item.model'; -import { Feature } from './shared/feature.model'; -import { Authorization } from './shared/authorization.model'; -import { FeatureDataService } from './data/feature-authorization/feature-data.service'; -import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service'; -import { - SiteAdministratorGuard -} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { Registration } from './shared/registration.model'; -import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; -import { MetadataFieldDataService } from './data/metadata-field-data.service'; -import { TokenResponseParsingService } from './auth/token-response-parsing.service'; -import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; -import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; -import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; -import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; -import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; -import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; -import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; -import { VocabularyService } from './submission/vocabularies/vocabulary.service'; -import { ConfigurationDataService } from './data/configuration-data.service'; -import { ConfigurationProperty } from './shared/configuration-property.model'; -import { ReloadGuard } from './reload/reload.guard'; -import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard'; -import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard'; -import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; -import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; -import { ShortLivedToken } from './auth/models/short-lived-token.model'; -import { UsageReport } from './statistics/models/usage-report.model'; -import { RootDataService } from './data/root-data.service'; -import { Root } from './data/root.model'; -import { SearchConfig } from './shared/search/search-filters/search-config.model'; -import { SequenceService } from './shared/sequence.service'; -import { CoreState } from './core-state.model'; -import { GroupDataService } from './eperson/group-data.service'; -import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; -import { QualityAssuranceTopicObject } from './notifications/qa/models/quality-assurance-topic.model'; -import { QualityAssuranceEventObject } from './notifications/qa/models/quality-assurance-event.model'; -import { QualityAssuranceSourceObject } from './notifications/qa/models/quality-assurance-source.model'; -import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model'; -import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model'; -import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model'; -import { AccessStatusObject } from '../shared/object-collection/shared/badges/access-status-badge/access-status.model'; -import { AccessStatusDataService } from './data/access-status-data.service'; -import { LinkHeadService } from './services/link-head.service'; -import { ResearcherProfileDataService } from './profile/researcher-profile-data.service'; -import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; -import { ResearcherProfile } from './profile/model/researcher-profile.model'; -import { OrcidQueueDataService } from './orcid/orcid-queue-data.service'; -import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; -import { OrcidQueue } from './orcid/model/orcid-queue.model'; -import { OrcidHistory } from './orcid/model/orcid-history.model'; -import { OrcidAuthService } from './orcid/orcid-auth.service'; -import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service'; -import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service'; -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'; -import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-definition.model'; -import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; -import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; -import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; -import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; -import { CorrectionTypeDataService } from './submission/correctiontype-data.service'; -import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; -import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; -import { - CoarNotifyConfigDataService -} from '../submission/sections/section-coar-notify/coar-notify-config-data.service'; -import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service'; -import { SuggestionTarget } from './notifications/models/suggestion-target.model'; -import { SuggestionSource } from './notifications/models/suggestion-source.model'; -import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model'; -import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; -import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; -import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; - -/** - * When not in production, endpoint responses can be mocked for testing purposes - * If there is no mock version available for the endpoint, the actual REST response will be used just like in production mode - */ -export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => { - if (environment.production) { - return new DspaceRestService(http); - } else { - return new EndpointMockingRestService(mocks, http); - } -}; - -const IMPORTS = [ - CommonModule, - StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(coreEffects) -]; - -const DECLARATIONS = []; - -const EXPORTS = []; - -const PROVIDERS = [ - AuthenticatedGuard, - CommunityDataService, - CollectionDataService, - SiteDataService, - DSOResponseParsingService, - { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, - { provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, - EPersonDataService, - LinkHeadService, - HALEndpointService, - HostWindowService, - ItemDataService, - MetadataService, - ObjectCacheService, - PaginationComponentOptions, - ResourcePolicyDataService, - RegistryService, - BitstreamFormatDataService, - RemoteDataBuildService, - EndpointMapResponseParsingService, - FacetValueResponseParsingService, - FacetConfigResponseParsingService, - DebugResponseParsingService, - SearchResponseParsingService, - MyDSpaceResponseParsingService, - ServerResponseService, - BrowseService, - AccessStatusDataService, - SubmissionCcLicenseDataService, - SubmissionCcLicenseUrlDataService, - SubmissionFormsConfigDataService, - SubmissionRestService, - SubmissionResponseParsingService, - SubmissionJsonPatchOperationsService, - JsonPatchOperationsBuilder, - UUIDService, - NotificationsService, - WorkspaceitemDataService, - WorkflowItemDataService, - DSpaceObjectDataService, - ConfigurationDataService, - DSOChangeAnalyzer, - DefaultChangeAnalyzer, - ArrayMoveChangeAnalyzer, - ObjectSelectService, - MenuService, - ObjectUpdatesService, - SearchService, - RelationshipDataService, - MyDSpaceGuard, - RoleService, - TaskResponseParsingService, - ClaimedTaskDataService, - PoolTaskDataService, - BitstreamDataService, - EntityTypeDataService, - ContentSourceResponseParsingService, - ItemTemplateDataService, - SearchService, - SidebarService, - SearchFilterService, - SearchFilterService, - SearchConfigurationService, - SelectableListService, - RelationshipTypeDataService, - ExternalSourceDataService, - LookupRelationService, - VersionDataService, - VersionHistoryDataService, - WorkflowActionDataService, - ProcessDataService, - ScriptDataService, - FeatureDataService, - AuthorizationDataService, - SiteAdministratorGuard, - SiteRegisterGuard, - MetadataSchemaDataService, - MetadataFieldDataService, - TokenResponseParsingService, - ReloadGuard, - EndUserAgreementCurrentUserGuard, - EndUserAgreementCookieGuard, - EndUserAgreementService, - RootDataService, - NotificationsService, - FilteredDiscoveryPageResponseParsingService, - { provide: NativeWindowService, useFactory: NativeWindowFactory }, - VocabularyService, - VocabularyDataService, - VocabularyEntryDetailsDataService, - SequenceService, - GroupDataService, - FeedbackDataService, - ResearcherProfileDataService, - ProfileClaimService, - OrcidAuthService, - OrcidQueueDataService, - OrcidHistoryDataService, - SupervisionOrderDataService, - CorrectionTypeDataService, - LdnServicesService, - LdnItemfiltersService, - CoarNotifyConfigDataService, - NotifyRequestsStatusDataService -]; - -/** - * Declaration needed to make sure all decorator functions are called in time - */ -export const models = - [ - Root, - DSpaceObject, - Bundle, - Bitstream, - BitstreamFormat, - Item, - Site, - Collection, - Community, - EPerson, - Group, - ResourcePolicy, - MetadataSchema, - MetadataField, - License, - WorkflowItem, - WorkspaceItem, - SubmissionCcLicence, - SubmissionCcLicenceUrl, - SubmissionDefinitionsModel, - SubmissionFormsModel, - SubmissionSectionModel, - SubmissionUploadsModel, - AuthStatus, - BrowseEntry, - BrowseDefinition, - NonHierarchicalBrowseDefinition, - FlatBrowseDefinition, - ValueListBrowseDefinition, - HierarchicalBrowseDefinition, - ClaimedTask, - TaskObject, - PoolTask, - Relationship, - RelationshipType, - ItemType, - ExternalSource, - ExternalSourceEntry, - Script, - Process, - Version, - VersionHistory, - WorkflowAction, - AdvancedWorkflowInfo, - RatingAdvancedWorkflowInfo, - SelectReviewerAdvancedWorkflowInfo, - TemplateItem, - Feature, - Authorization, - Registration, - Vocabulary, - VocabularyEntry, - VocabularyEntryDetail, - ConfigurationProperty, - ShortLivedToken, - Registration, - UsageReport, - QualityAssuranceTopicObject, - QualityAssuranceEventObject, - Root, - SearchConfig, - SubmissionAccessesModel, - QualityAssuranceSourceObject, - AccessStatusObject, - ResearcherProfile, - OrcidQueue, - OrcidHistory, - AccessStatusObject, - IdentifierData, - Subscription, - ItemRequest, - BulkAccessConditionOptions, - SuggestionTarget, - SuggestionSource, - LdnService, - Itemfilter, - SubmissionCoarNotifyConfig, - NotifyRequestsStatus, - ]; - -@NgModule({ - imports: [ - ...IMPORTS - ], - declarations: [ - ...DECLARATIONS - ], - exports: [ - ...EXPORTS - ], - providers: [ - ...PROVIDERS - ] -}) - -export class CoreModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: CoreModule, - providers: [ - ...PROVIDERS - ] - }; - } - - constructor(@Optional() @SkipSelf() parentModule: CoreModule) { - if (isNotEmpty(parentModule)) { - throw new Error('CoreModule is already loaded. Import it in the AppModule only'); - } - } -} diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index c0165c53848..fda1e05df05 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -1,19 +1,17 @@ -import { ActionReducerMap, } from '@ngrx/store'; +import { ActionReducerMap } from '@ngrx/store'; -import { objectCacheReducer } from './cache/object-cache.reducer'; -import { indexReducer } from './index/index.reducer'; -import { requestReducer } from './data/request.reducer'; +import { bitstreamFormatReducer } from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { authReducer } from './auth/auth.reducer'; -import { jsonPatchOperationsReducer } from './json-patch/json-patch-operations.reducer'; +import { objectCacheReducer } from './cache/object-cache.reducer'; import { serverSyncBufferReducer } from './cache/server-sync-buffer.reducer'; +import { CoreState } from './core-state.model'; import { objectUpdatesReducer } from './data/object-updates/object-updates.reducer'; -import { routeReducer } from './services/route.reducer'; -import { - bitstreamFormatReducer -} from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { requestReducer } from './data/request.reducer'; import { historyReducer } from './history/history.reducer'; +import { indexReducer } from './index/index.reducer'; +import { jsonPatchOperationsReducer } from './json-patch/json-patch-operations.reducer'; import { metaTagReducer } from './metadata/meta-tag.reducer'; -import { CoreState } from './core-state.model'; +import { routeReducer } from './services/route.reducer'; export const coreReducers: ActionReducerMap = { 'bitstreamFormats': bitstreamFormatReducer, @@ -26,5 +24,5 @@ export const coreReducers: ActionReducerMap = { 'auth': authReducer, 'json/patch': jsonPatchOperationsReducer, 'metaTag': metaTagReducer, - 'route': routeReducer + 'route': routeReducer, }; diff --git a/src/app/core/core.selectors.ts b/src/app/core/core.selectors.ts index 77c7974de2c..899afb9be98 100644 --- a/src/app/core/core.selectors.ts +++ b/src/app/core/core.selectors.ts @@ -1,4 +1,5 @@ import { createFeatureSelector } from '@ngrx/store'; + import { CoreState } from './core-state.model'; /** diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts new file mode 100644 index 00000000000..8e76eef9258 --- /dev/null +++ b/src/app/core/data-services-map.ts @@ -0,0 +1,139 @@ +import { LazyDataServicesMap } from '../../config/app-config.interface'; +import { + LDN_SERVICE, + LDN_SERVICE_CONSTRAINT_FILTERS, +} from '../admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type'; +import { ADMIN_NOTIFY_MESSAGE } from '../admin/admin-notify-dashboard/models/admin-notify-message.resource-type'; +import { NOTIFYREQUEST } from '../item-page/simple/notify-requests-status/notify-requests-status.resource-type'; +import { PROCESS } from '../process-page/processes/process.resource-type'; +import { SCRIPT } from '../process-page/scripts/script.resource-type'; +import { ACCESS_STATUS } from '../shared/object-collection/shared/badges/access-status-badge/access-status.resource-type'; +import { DUPLICATE } from '../shared/object-list/duplicate-data/duplicate.resource-type'; +import { IDENTIFIERS } from '../shared/object-list/identifier-data/identifier-data.resource-type'; +import { SUBSCRIPTION } from '../shared/subscriptions/models/subscription.resource-type'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from '../submission/sections/section-coar-notify/section-coar-notify-service.resource-type'; +import { SYSTEMWIDEALERT } from '../system-wide-alert/system-wide-alert.resource-type'; +import { + BULK_ACCESS_CONDITION_OPTIONS, + SUBMISSION_ACCESSES_TYPE, + SUBMISSION_FORMS_TYPE, + SUBMISSION_UPLOADS_TYPE, +} from './config/models/config-type'; +import { ROOT } from './data/root.resource-type'; +import { EPERSON } from './eperson/models/eperson.resource-type'; +import { GROUP } from './eperson/models/group.resource-type'; +import { WORKFLOWITEM } from './eperson/models/workflowitem.resource-type'; +import { WORKSPACEITEM } from './eperson/models/workspaceitem.resource-type'; +import { FEEDBACK } from './feedback/models/feedback.resource-type'; +import { METADATA_FIELD } from './metadata/metadata-field.resource-type'; +import { METADATA_SCHEMA } from './metadata/metadata-schema.resource-type'; +import { SUGGESTION } from './notifications/models/suggestion-objects.resource-type'; +import { SUGGESTION_SOURCE } from './notifications/models/suggestion-source-object.resource-type'; +import { SUGGESTION_TARGET } from './notifications/models/suggestion-target-object.resource-type'; +import { QUALITY_ASSURANCE_EVENT_OBJECT } from './notifications/qa/models/quality-assurance-event-object.resource-type'; +import { QUALITY_ASSURANCE_SOURCE_OBJECT } from './notifications/qa/models/quality-assurance-source-object.resource-type'; +import { QUALITY_ASSURANCE_TOPIC_OBJECT } from './notifications/qa/models/quality-assurance-topic-object.resource-type'; +import { ORCID_HISTORY } from './orcid/model/orcid-history.resource-type'; +import { ORCID_QUEUE } from './orcid/model/orcid-queue.resource-type'; +import { RESEARCHER_PROFILE } from './profile/model/researcher-profile.resource-type'; +import { RESOURCE_POLICY } from './resource-policy/models/resource-policy.resource-type'; +import { AUTHORIZATION } from './shared/authorization.resource-type'; +import { BITSTREAM } from './shared/bitstream.resource-type'; +import { BITSTREAM_FORMAT } from './shared/bitstream-format.resource-type'; +import { BROWSE_DEFINITION } from './shared/browse-definition.resource-type'; +import { BUNDLE } from './shared/bundle.resource-type'; +import { COLLECTION } from './shared/collection.resource-type'; +import { COMMUNITY } from './shared/community.resource-type'; +import { CONFIG_PROPERTY } from './shared/config-property.resource-type'; +import { DSPACE_OBJECT } from './shared/dspace-object.resource-type'; +import { FEATURE } from './shared/feature.resource-type'; +import { ITEM } from './shared/item.resource-type'; +import { ITEM_TYPE } from './shared/item-relationships/item-type.resource-type'; +import { RELATIONSHIP } from './shared/item-relationships/relationship.resource-type'; +import { RELATIONSHIP_TYPE } from './shared/item-relationships/relationship-type.resource-type'; +import { LICENSE } from './shared/license.resource-type'; +import { SITE } from './shared/site.resource-type'; +import { VERSION } from './shared/version.resource-type'; +import { VERSION_HISTORY } from './shared/version-history.resource-type'; +import { USAGE_REPORT } from './statistics/models/usage-report.resource-type'; +import { CorrectionType } from './submission/models/correctiontype.model'; +import { SUBMISSION_CC_LICENSE } from './submission/models/submission-cc-licence.resource-type'; +import { SUBMISSION_CC_LICENSE_URL } from './submission/models/submission-cc-licence-link.resource-type'; +import { + VOCABULARY, + VOCABULARY_ENTRY, + VOCABULARY_ENTRY_DETAIL, +} from './submission/vocabularies/models/vocabularies.resource-type'; +import { SUPERVISION_ORDER } from './supervision-order/models/supervision-order.resource-type'; +import { CLAIMED_TASK } from './tasks/models/claimed-task-object.resource-type'; +import { POOL_TASK } from './tasks/models/pool-task-object.resource-type'; +import { WORKFLOW_ACTION } from './tasks/models/workflow-action-object.resource-type'; + +export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ + [AUTHORIZATION.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [BROWSE_DEFINITION.value, () => import('./browse/browse-definition-data.service').then(m => m.BrowseDefinitionDataService)], + [BULK_ACCESS_CONDITION_OPTIONS.value, () => import('./config/bulk-access-config-data.service').then(m => m.BulkAccessConfigDataService)], + [METADATA_SCHEMA.value, () => import('./data/metadata-schema-data.service').then(m => m.MetadataSchemaDataService)], + [SUBMISSION_UPLOADS_TYPE.value, () => import('./config/submission-uploads-config-data.service').then(m => m.SubmissionUploadsConfigDataService)], + [BITSTREAM.value, () => import('./data/bitstream-data.service').then(m => m.BitstreamDataService)], + [SUBMISSION_ACCESSES_TYPE.value, () => import('./config/submission-accesses-config-data.service').then(m => m.SubmissionAccessesConfigDataService)], + [SYSTEMWIDEALERT.value, () => import('./data/system-wide-alert-data.service').then(m => m.SystemWideAlertDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [ACCESS_STATUS.value, () => import('./data/access-status-data.service').then(m => m.AccessStatusDataService)], + [COLLECTION.value, () => import('./data/collection-data.service').then(m => m.CollectionDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [VOCABULARY_ENTRY.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [ITEM_TYPE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [LICENSE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [SUBSCRIPTION.value, () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService)], + [COMMUNITY.value, () => import('./data/community-data.service').then(m => m.CommunityDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [BUNDLE.value, () => import('./data/bundle-data.service').then(m => m.BundleDataService)], + [CONFIG_PROPERTY.value, () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService)], + [POOL_TASK.value, () => import('./tasks/pool-task-data.service').then(m => m.PoolTaskDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [SUPERVISION_ORDER.value, () => import('./supervision-order/supervision-order-data.service').then(m => m.SupervisionOrderDataService)], + [WORKSPACEITEM.value, () => import('./submission/workspaceitem-data.service').then(m => m.WorkspaceitemDataService)], + [WORKFLOWITEM.value, () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [VOCABULARY_ENTRY_DETAIL.value, () => import('./submission/vocabularies/vocabulary-entry-details.data.service').then(m => m.VocabularyEntryDetailsDataService)], + [SUBMISSION_CC_LICENSE_URL.value, () => import('./submission/submission-cc-license-url-data.service').then(m => m.SubmissionCcLicenseUrlDataService)], + [SUBMISSION_CC_LICENSE.value, () => import('./submission/submission-cc-license-data.service').then(m => m.SubmissionCcLicenseDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [RESOURCE_POLICY.value, () => import('./resource-policy/resource-policy-data.service').then(m => m.ResourcePolicyDataService)], + [RESEARCHER_PROFILE.value, () => import('./profile/researcher-profile-data.service').then(m => m.ResearcherProfileDataService)], + [ORCID_QUEUE.value, () => import('./orcid/orcid-queue-data.service').then(m => m.OrcidQueueDataService)], + [ORCID_HISTORY.value, () => import('./orcid/orcid-history-data.service').then(m => m.OrcidHistoryDataService)], + [FEEDBACK.value, () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService)], + [GROUP.value, () => import('./eperson/group-data.service').then(m => m.GroupDataService)], + [EPERSON.value, () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService)], + [WORKFLOW_ACTION.value, () => import('./data/workflow-action-data.service').then(m => m.WorkflowActionDataService)], + [VERSION_HISTORY.value, () => import('./data/version-history-data.service').then(m => m.VersionHistoryDataService)], + [SITE.value, () => import('./data/site-data.service').then(m => m.SiteDataService)], + [ROOT.value, () => import('./data/root-data.service').then(m => m.RootDataService)], + [RELATIONSHIP_TYPE.value, () => import('./data/relationship-type-data.service').then(m => m.RelationshipTypeDataService)], + [RELATIONSHIP.value, () => import('./data/relationship-data.service').then(m => m.RelationshipDataService)], + [SCRIPT.value, () => import('./data/processes/script-data.service').then(m => m.ScriptDataService)], + [PROCESS.value, () => import('./data/processes/process-data.service').then(m => m.ProcessDataService)], + [METADATA_FIELD.value, () => import('./data/metadata-field-data.service').then(m => m.MetadataFieldDataService)], + [ITEM.value, () => import('./data/item-data.service').then(m => m.ItemDataService)], + [VERSION.value, () => import('./data/version-data.service').then(m => m.VersionDataService)], + [IDENTIFIERS.value, () => import('./data/identifier-data.service').then(m => m.IdentifierDataService)], + [FEATURE.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [DSPACE_OBJECT.value, () => import('./data/dspace-object-data.service').then(m => m.DSpaceObjectDataService)], + [BITSTREAM_FORMAT.value, () => import('./data/bitstream-format-data.service').then(m => m.BitstreamFormatDataService)], + [SUBMISSION_COAR_NOTIFY_CONFIG.value, () => import('../submission/sections/section-coar-notify/coar-notify-config-data.service').then(m => m.CoarNotifyConfigDataService)], + [LDN_SERVICE_CONSTRAINT_FILTERS.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service').then(m => m.LdnItemfiltersService)], + [LDN_SERVICE.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service').then(m => m.LdnServicesService)], + [ADMIN_NOTIFY_MESSAGE.value, () => import('../admin/admin-notify-dashboard/services/admin-notify-messages.service').then(m => m.AdminNotifyMessagesService)], + [SUBMISSION_FORMS_TYPE.value, () => import('./config/submission-forms-config-data.service').then(m => m.SubmissionFormsConfigDataService)], + [NOTIFYREQUEST.value, () => import('./data/notify-services-status-data.service').then(m => m.NotifyRequestsStatusDataService)], + [QUALITY_ASSURANCE_EVENT_OBJECT.value, () => import('./notifications/qa/events/quality-assurance-event-data.service').then(m => m.QualityAssuranceEventDataService)], + [QUALITY_ASSURANCE_SOURCE_OBJECT.value, () => import('./notifications/qa/source/quality-assurance-source-data.service').then(m => m.QualityAssuranceSourceDataService)], + [QUALITY_ASSURANCE_TOPIC_OBJECT.value, () => import('./notifications/qa/topics/quality-assurance-topic-data.service').then(m => m.QualityAssuranceTopicDataService)], + [SUGGESTION.value, () => import('./notifications/suggestions-data.service').then(m => m.SuggestionsDataService)], + [SUGGESTION_SOURCE.value, () => import('./notifications/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService)], + [SUGGESTION_TARGET.value, () => import('./notifications/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], + [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)], + [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)], +]); diff --git a/src/app/core/data/access-status-data.service.spec.ts b/src/app/core/data/access-status-data.service.spec.ts index 18b8cb5d65d..1240585027f 100644 --- a/src/app/core/data/access-status-data.service.spec.ts +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -1,17 +1,21 @@ -import { RequestService } from './request.service'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { Observable } from 'rxjs'; + +import { hasNoValue } from '../../shared/empty.util'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { GetRequest } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { hasNoValue } from '../../shared/empty.util'; -import { AccessStatusDataService } from './access-status-data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Item } from '../shared/item.model'; +import { AccessStatusDataService } from './access-status-data.service'; +import { RemoteData } from './remote-data'; +import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; const url = 'fake-url'; @@ -29,12 +33,12 @@ describe('AccessStatusDataService', () => { name: 'test-item', _links: { accessStatus: { - href: `https://rest.api/items/${itemId}/accessStatus` + href: `https://rest.api/items/${itemId}/accessStatus`, }, self: { - href: `https://rest.api/items/${itemId}` - } - } + href: `https://rest.api/items/${itemId}`, + }, + }, }); describe('when the requests are successful', () => { @@ -69,10 +73,10 @@ describe('AccessStatusDataService', () => { } rdbService = jasmine.createSpyObj('rdbService', { buildFromRequestUUID: buildResponse$, - buildSingle: buildResponse$ + buildSingle: buildResponse$, }); objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); halService = new HALEndpointServiceStub(url); notificationsService = new NotificationsServiceStub(); diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts index e8b77245e87..6d8acb1c8b4 100644 --- a/src/app/core/data/access-status-data.service.ts +++ b/src/app/core/data/access-status-data.service.ts @@ -1,21 +1,19 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; -import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model'; -import { ACCESS_STATUS } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.resource-type'; -import { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; import { Item } from '../shared/item.model'; import { BaseDataService } from './base/base-data.service'; -import { dataService } from './base/data-service.decorator'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * Data service responsible for retrieving the access status of Items */ -@Injectable() -@dataService(ACCESS_STATUS) +@Injectable({ providedIn: 'root' }) export class AccessStatusDataService extends BaseDataService { constructor( diff --git a/src/app/core/data/array-move-change-analyzer.service.spec.ts b/src/app/core/data/array-move-change-analyzer.service.spec.ts index 025791d6dc4..cf11decde4c 100644 --- a/src/app/core/data/array-move-change-analyzer.service.spec.ts +++ b/src/app/core/data/array-move-change-analyzer.service.spec.ts @@ -1,7 +1,8 @@ -import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; import { moveItemInArray } from '@angular/cdk/drag-drop'; import { Operation } from 'fast-json-patch'; +import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; + /** * Helper class for creating move tests * Define a "from" and "to" index to move objects within the array before comparing @@ -28,7 +29,7 @@ describe('ArrayMoveChangeAnalyzer', () => { '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', 'e56eb99e-2f7c-4bee-9b3f-d3dcc83386b1', '0f608168-cdfc-46b0-92ce-889f7d3ac684', - '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + '546f9f5c-15dc-4eec-86fe-648007ac9e1c', ]; }); @@ -72,7 +73,7 @@ describe('ArrayMoveChangeAnalyzer', () => { '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', undefined, undefined, - '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + '546f9f5c-15dc-4eec-86fe-648007ac9e1c', ]; }); diff --git a/src/app/core/data/array-move-change-analyzer.service.ts b/src/app/core/data/array-move-change-analyzer.service.ts index 36744e9f96e..bd5fc8dedb3 100644 --- a/src/app/core/data/array-move-change-analyzer.service.ts +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -1,12 +1,13 @@ -import { MoveOperation } from 'fast-json-patch'; -import { Injectable } from '@angular/core'; import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { Injectable } from '@angular/core'; +import { MoveOperation } from 'fast-json-patch'; + import { hasValue } from '../../shared/empty.util'; /** * A class to determine move operations between two arrays */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ArrayMoveChangeAnalyzer { /** diff --git a/src/app/core/data/base-response-parsing.service.spec.ts b/src/app/core/data/base-response-parsing.service.spec.ts index da9fa7a6432..f6707d3582a 100644 --- a/src/app/core/data/base-response-parsing.service.spec.ts +++ b/src/app/core/data/base-response-parsing.service.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable max-classes-per-file */ -import { BaseResponseParsingService } from './base-response-parsing.service'; +import { CacheableObject } from '../cache/cacheable-object.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GetRequest} from './request.models'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { GetRequest } from './request.models'; import { RestRequest } from './rest-request.model'; class TestService extends BaseResponseParsingService { @@ -35,7 +35,7 @@ describe('BaseResponseParsingService', () => { beforeEach(() => { obj = undefined; objectCache = jasmine.createSpyObj('objectCache', { - add: {} + add: {}, }); service = new TestService(objectCache); }); @@ -58,8 +58,8 @@ describe('BaseResponseParsingService', () => { beforeEach(() => { obj = Object.assign(new DSpaceObject(), { _links: { - self: { href: 'obj-selflink' } - } + self: { href: 'obj-selflink' }, + }, }); }); @@ -79,8 +79,8 @@ describe('BaseResponseParsingService', () => { data = { type: 'NotARealType', _links: { - self: { href: 'data-selflink' } - } + self: { href: 'data-selflink' }, + }, }; }); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 18e6623683f..63b4961b313 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -1,14 +1,21 @@ /* eslint-disable max-classes-per-file */ -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { environment } from '../../../environments/environment'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { Serializer } from '../serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { GenericConstructor } from '../shared/generic-constructor'; -import { PaginatedList, buildPaginatedList } from './paginated-list.model'; -import { getClassForType } from '../cache/builders/build-decorators'; -import { environment } from '../../../environments/environment'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { PageInfo } from '../shared/page-info.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; import { RestRequest } from './rest-request.model'; @@ -64,7 +71,7 @@ export abstract class BaseResponseParsingService { } else if (isRestDataObject(data._embedded[property])) { object[property] = this.retrieveObjectOrUrl(parsedObj); } else if (Array.isArray(parsedObj)) { - object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)); + object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)); } } }); @@ -96,14 +103,14 @@ export abstract class BaseResponseParsingService { list = this.flattenSingleKeyObject(list); } const page: ObjectDomain[] = this.processArray(list, request); - return buildPaginatedList(pageInfo, page,); + return buildPaginatedList(pageInfo, page); } protected processArray(data: any, request: RestRequest): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { - array = [...array, this.process(datum, request)]; - } + array = [...array, this.process(datum, request)]; + }, ); return array; } @@ -139,7 +146,7 @@ export abstract class BaseResponseParsingService { let dataJSON: string; if (hasValue(data._embedded)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 3366209179d..3f44ad5e5ac 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -5,26 +5,37 @@ * * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; -import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { HALLink } from '../../shared/hal-link.model'; +import { FindListOptions } from '../find-list-options.model'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { fakeAsync, tick } from '@angular/core/testing'; import { BaseDataService } from './base-data.service'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub'; -import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; -import { HALLink } from '../../shared/hal-link.model'; -import { createPaginatedList } from '../../../shared/testing/utils.test'; const endpoint = 'https://rest.api/core'; @@ -65,7 +76,7 @@ describe('BaseDataService', () => { selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { @@ -95,7 +106,7 @@ describe('BaseDataService', () => { href: 'follow-link-2-2', }), ], - } + }, }; const statusCodeSuccess = 200; const statusCodeError = 404; @@ -635,7 +646,7 @@ describe('BaseDataService', () => { beforeEach(() => { getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ requestUUIDs: ['request1', 'request2', 'request3'], - dependentRequestUUIDs: ['request4', 'request5'] + dependentRequestUUIDs: ['request4', 'request5'], } as ObjectCacheEntry)); }); @@ -783,7 +794,7 @@ describe('BaseDataService', () => { (service as any).addDependency( createSuccessfulRemoteDataObject$({ _links: { self: { href: 'object-href' } } }), - observableOf('dependsOnHref') + observableOf('dependsOnHref'), ); expect(addDependencySpy).toHaveBeenCalled(); }); @@ -798,7 +809,7 @@ describe('BaseDataService', () => { (service as any).addDependency( createFailedRemoteDataObject$('something went wrong'), - observableOf('dependsOnHref') + observableOf('dependsOnHref'), ); expect(addDependencySpy).toHaveBeenCalled(); }); diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index 5694cd77911..d09ee21ee03 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -6,25 +6,43 @@ * http://www.dspace.org/license/ */ -import { AsyncSubject, from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { map, mergeMap, skipWhile, switchMap, take, tap, toArray } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; +import { + AsyncSubject, + from as observableFrom, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + mergeMap, + skipWhile, + switchMap, + take, + tap, + toArray, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { RequestParam } from '../../cache/models/request-param.model'; +import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; +import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { HALLink } from '../../shared/hal-link.model'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; import { RemoteData } from '../remote-data'; import { GetRequest } from '../request.models'; import { RequestService } from '../request.service'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { FindListOptions } from '../find-list-options.model'; -import { PaginatedList } from '../paginated-list.model'; -import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; -import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALDataService } from './hal-data-service.interface'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; -import { HALLink } from '../../shared/hal-link.model'; export const EMBED_SEPARATOR = '%2F'; /** @@ -236,7 +254,7 @@ export class BaseDataService implements HALDataServic if (hasValue(remoteData) && remoteData.isStale) { requestFn(); } - }) + }), ); } else { return source; @@ -372,7 +390,7 @@ export class BaseDataService implements HALDataServic href$.pipe( isNotEmptyOperator(), - take(1) + take(1), ).subscribe((href: string) => { const requestId = this.requestService.generateRequestId(); const request = new GetRequest(requestId, href); @@ -416,13 +434,13 @@ export class BaseDataService implements HALDataServic return this.hasCachedResponse(href$).pipe( switchMap((hasCachedResponse) => { if (hasCachedResponse) { - return this.rdbService.buildSingle(href$).pipe( - getFirstCompletedRemoteData(), - map((rd => rd.hasFailed)) - ); + return this.rdbService.buildSingle(href$).pipe( + getFirstCompletedRemoteData(), + map((rd => rd.hasFailed)), + ); } return observableOf(false); - }) + }), ); } @@ -463,7 +481,7 @@ export class BaseDataService implements HALDataServic } }), ), - dependsOnHref$ + dependsOnHref$, ); } @@ -480,7 +498,7 @@ export class BaseDataService implements HALDataServic switchMap((oce: ObjectCacheEntry) => { return observableFrom([ ...oce.requestUUIDs, - ...oce.dependentRequestUUIDs + ...oce.dependentRequestUUIDs, ]).pipe( mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), toArray(), diff --git a/src/app/core/data/base/create-data.spec.ts b/src/app/core/data/base/create-data.spec.ts index 0b2e0f39308..1248c5ffe49 100644 --- a/src/app/core/data/base/create-data.spec.ts +++ b/src/app/core/data/base/create-data.spec.ts @@ -5,23 +5,33 @@ * * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, +} from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { RequestParam } from '../../cache/models/request-param.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { CreateData, CreateDataImpl } from './create-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { RequestParam } from '../../cache/models/request-param.model'; import { RestRequestMethod } from '../rest-request-method'; -import { DSpaceObject } from '../../shared/dspace-object.model'; +import { + CreateData, + CreateDataImpl, +} from './create-data'; /** * Tests whether calls to `CreateData` methods are correctly patched through in a concrete data service that implements it @@ -149,7 +159,7 @@ describe('CreateDataImpl', () => { describe('create', () => { it('should POST the object to the root endpoint with the given parameters and return the remote data', (done) => { const params = [ - new RequestParam('abc', 123), new RequestParam('def', 456) + new RequestParam('abc', 123), new RequestParam('def', 456), ]; buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success)); diff --git a/src/app/core/data/base/create-data.ts b/src/app/core/data/base/create-data.ts index d2e009f6698..13216f8796d 100644 --- a/src/app/core/data/base/create-data.ts +++ b/src/app/core/data/base/create-data.ts @@ -5,22 +5,31 @@ * * http://www.dspace.org/license/ */ +import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + map, + take, + takeWhile, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmptyOperator, +} from '../../../shared/empty.util'; +import { NotificationOptions } from '../../../shared/notifications/models/notification-options.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { getClassForType } from '../../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { RequestParam } from '../../cache/models/request-param.model'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; -import { distinctUntilChanged, map, take, takeWhile } from 'rxjs/operators'; +import { ObjectCacheService } from '../../cache/object-cache.service'; import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; -import { getClassForType } from '../../cache/builders/build-decorators'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; import { CreateRequest } from '../request.models'; -import { NotificationOptions } from '../../../shared/notifications/models/notification-options.model'; import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that can create objects. @@ -95,7 +104,7 @@ export class CreateDataImpl extends BaseDataService) => rd.isLoading, true) + takeWhile((rd: RemoteData) => rd.isLoading, true), ).subscribe((rd: RemoteData) => { if (rd.hasFailed) { this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1)); diff --git a/src/app/core/data/base/data-service.decorator.spec.ts b/src/app/core/data/base/data-service.decorator.spec.ts deleted file mode 100644 index e09c531a569..00000000000 --- a/src/app/core/data/base/data-service.decorator.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable max-classes-per-file */ -/** - * 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/ - */ -import { ResourceType } from '../../shared/resource-type'; -import { BaseDataService } from './base-data.service'; -import { HALDataService } from './hal-data-service.interface'; -import { dataService, getDataServiceFor } from './data-service.decorator'; -import { v4 as uuidv4 } from 'uuid'; - -class TestService extends BaseDataService { -} - -class AnotherTestService implements HALDataService { - public findListByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } - - public findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } -} - -let testType; - -describe('@dataService/getDataServiceFor', () => { - beforeEach(() => { - testType = new ResourceType(`testType-${uuidv4()}`); - }); - - it('should register a resourcetype for a dataservice', () => { - dataService(testType)(TestService); - expect(getDataServiceFor(testType)).toBe(TestService); - }); - - describe(`when the resource type isn't specified`, () => { - it(`should throw an error`, () => { - expect(() => { - dataService(undefined)(TestService); - }).toThrow(); - }); - }); - - describe(`when there already is a registered dataservice for a resourcetype`, () => { - it(`should throw an error`, () => { - dataService(testType)(TestService); - expect(() => { - dataService(testType)(AnotherTestService); - }).toThrow(); - }); - }); -}); diff --git a/src/app/core/data/base/data-service.decorator.ts b/src/app/core/data/base/data-service.decorator.ts deleted file mode 100644 index fbde9bd94f8..00000000000 --- a/src/app/core/data/base/data-service.decorator.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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/ - */ -import { InjectionToken } from '@angular/core'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { ResourceType } from '../../shared/resource-type'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { HALDataService } from './hal-data-service.interface'; - -export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>>('getDataServiceFor', { - providedIn: 'root', - factory: () => getDataServiceFor, -}); -const dataServiceMap = new Map(); - -/** - * A class decorator to indicate that this class is a data service for a given HAL resource type. - * - * In most cases, a data service should extend {@link BaseDataService}. - * At the very least it must implement {@link HALDataService} in order for it to work with {@link LinkService}. - * - * @param resourceType the resource type the class is a dataservice for - */ -export function dataService(resourceType: ResourceType) { - return (target: GenericConstructor>): void => { - if (hasNoValue(resourceType)) { - throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`); - } - const existingDataservice = dataServiceMap.get(resourceType.value); - - if (hasValue(existingDataservice)) { - throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`); - } - - dataServiceMap.set(resourceType.value, target); - }; -} - -/** - * Return the dataservice matching the given resource type - * - * @param resourceType the resource type you want the matching dataservice for - */ -export function getDataServiceFor(resourceType: ResourceType): GenericConstructor> { - return dataServiceMap.get(resourceType.value); -} diff --git a/src/app/core/data/base/delete-data.spec.ts b/src/app/core/data/base/delete-data.spec.ts index a076473b0fe..53d651402f1 100644 --- a/src/app/core/data/base/delete-data.spec.ts +++ b/src/app/core/data/base/delete-data.spec.ts @@ -5,24 +5,35 @@ * * http://www.dspace.org/license/ */ -import { constructIdEndpointDefault } from './identifiable-data.service'; -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { DeleteData, DeleteDataImpl } from './delete-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { RestRequestMethod } from '../rest-request-method'; +import { + DeleteData, + DeleteDataImpl, +} from './delete-data'; +import { constructIdEndpointDefault } from './identifiable-data.service'; /** * Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it @@ -34,7 +45,7 @@ export function testDeleteDataImplementation(serviceFactory: () => DeleteData { @@ -105,13 +116,13 @@ describe('DeleteDataImpl', () => { }, getByHref: () => { /* empty */ - } + }, } as any; notificationsService = {} as NotificationsService; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { diff --git a/src/app/core/data/base/delete-data.ts b/src/app/core/data/base/delete-data.ts index 807d9d838e9..e758ee40fa0 100644 --- a/src/app/core/data/base/delete-data.ts +++ b/src/app/core/data/base/delete-data.ts @@ -5,19 +5,26 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { NoContent } from '../../shared/NoContent.model'; import { switchMap } from 'rxjs/operators'; -import { DeleteRequest } from '../request.models'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; + +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NoContent } from '../../shared/NoContent.model'; +import { RemoteData } from '../remote-data'; +import { DeleteRequest } from '../request.models'; +import { RequestService } from '../request.service'; +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './identifiable-data.service'; export interface DeleteData { /** diff --git a/src/app/core/data/base/find-all-data.spec.ts b/src/app/core/data/base/find-all-data.spec.ts index 6a73e032d07..f2c48feb76c 100644 --- a/src/app/core/data/base/find-all-data.spec.ts +++ b/src/app/core/data/base/find-all-data.spec.ts @@ -5,24 +5,33 @@ * * http://www.dspace.org/license/ */ -import { FindAllData, FindAllDataImpl } from './find-all-data'; -import { FindListOptions } from '../find-list-options.model'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; -import { RequestParam } from '../../cache/models/request-param.model'; -import { RequestService } from '../request.service'; +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { + SortDirection, + SortOptions, +} from '../../cache/models/sort-options.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { RequestEntryState } from '../request-entry-state.model'; import { EMBED_SEPARATOR } from './base-data.service'; +import { + FindAllData, + FindAllDataImpl, +} from './find-all-data'; /** * Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it @@ -143,8 +152,8 @@ describe('FindAllDataImpl', () => { options = {}; (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(endpoint); - }, + expect(value).toBe(endpoint); + }, ); }); diff --git a/src/app/core/data/base/find-all-data.ts b/src/app/core/data/base/find-all-data.ts index bc0c1fb613e..0d68689e611 100644 --- a/src/app/core/data/base/find-all-data.ts +++ b/src/app/core/data/base/find-all-data.ts @@ -7,18 +7,23 @@ */ import { Observable } from 'rxjs'; -import { FindListOptions } from '../find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list.model'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, +} from 'rxjs/operators'; + import { isNotEmpty } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that list all of its objects. @@ -87,10 +92,9 @@ export class FindAllDataImpl extends BaseDataService< * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig[]): Observable { - let endpoint$: Observable; const args = []; - endpoint$ = this.getBrowseEndpoint(options).pipe( + const endpoint$ = this.getBrowseEndpoint(options).pipe( filter((href: string) => isNotEmpty(href)), map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href), distinctUntilChanged(), diff --git a/src/app/core/data/base/hal-data-service.interface.ts b/src/app/core/data/base/hal-data-service.interface.ts index 6959399760c..1ffdffaa7c5 100644 --- a/src/app/core/data/base/hal-data-service.interface.ts +++ b/src/app/core/data/base/hal-data-service.interface.ts @@ -6,11 +6,12 @@ * http://www.dspace.org/license/ */ import { Observable } from 'rxjs'; + import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; +import { HALResource } from '../../shared/hal-resource.model'; import { FindListOptions } from '../find-list-options.model'; import { PaginatedList } from '../paginated-list.model'; -import { HALResource } from '../../shared/hal-resource.model'; +import { RemoteData } from '../remote-data'; /** * An interface defining the minimum functionality needed for a data service to resolve HAL resources. diff --git a/src/app/core/data/base/identifiable-data.service.spec.ts b/src/app/core/data/base/identifiable-data.service.spec.ts index 11af83ff9f6..528d6c4945f 100644 --- a/src/app/core/data/base/identifiable-data.service.spec.ts +++ b/src/app/core/data/base/identifiable-data.service.spec.ts @@ -5,21 +5,25 @@ * * http://www.dspace.org/license/ */ -import { FindListOptions } from '../find-list-options.model'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { IdentifiableDataService } from './identifiable-data.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { RequestEntryState } from '../request-entry-state.model'; import { EMBED_SEPARATOR } from './base-data.service'; +import { IdentifiableDataService } from './identifiable-data.service'; const endpoint = 'https://rest.api/core'; @@ -63,12 +67,12 @@ describe('IdentifiableDataService', () => { }, getByHref: () => { /* empty */ - } + }, } as any; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { @@ -132,7 +136,7 @@ describe('IdentifiableDataService', () => { resourceIdMock, followLink('bundles', { shouldEmbed: false }), followLink('owningCollection', { shouldEmbed: false }), - followLink('templateItemOf') + followLink('templateItemOf'), ); expect(result).toEqual(expected); }); diff --git a/src/app/core/data/base/identifiable-data.service.ts b/src/app/core/data/base/identifiable-data.service.ts index 904f925765c..da3167903e0 100644 --- a/src/app/core/data/base/identifiable-data.service.ts +++ b/src/app/core/data/base/identifiable-data.service.ts @@ -5,16 +5,17 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { RemoteData } from '../remote-data'; -import { BaseDataService } from './base-data.service'; -import { RequestService } from '../request.service'; + +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Shorthand type for the method to construct an ID endpoint. diff --git a/src/app/core/data/base/patch-data.spec.ts b/src/app/core/data/base/patch-data.spec.ts index a55b1229b87..03c15199a7c 100644 --- a/src/app/core/data/base/patch-data.spec.ts +++ b/src/app/core/data/base/patch-data.spec.ts @@ -6,28 +6,38 @@ * http://www.dspace.org/license/ */ /* eslint-disable max-classes-per-file */ -import { RequestService } from '../request.service'; +import { + compare, + Operation, +} from 'fast-json-patch'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { Item } from '../../shared/item.model'; +import { ChangeAnalyzer } from '../change-analyzer'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { PatchData, PatchDataImpl } from './patch-data'; -import { ChangeAnalyzer } from '../change-analyzer'; -import { Item } from '../../shared/item.model'; -import { compare, Operation } from 'fast-json-patch'; import { PatchRequest } from '../request.models'; -import { DSpaceObject } from '../../shared/dspace-object.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { constructIdEndpointDefault } from './identifiable-data.service'; +import { RequestService } from '../request.service'; +import { RequestEntryState } from '../request-entry-state.model'; import { RestRequestMethod } from '../rest-request-method'; +import { constructIdEndpointDefault } from './identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './patch-data'; /** * Tests whether calls to `PatchData` methods are correctly patched through in a concrete data service that implements it @@ -182,15 +192,15 @@ describe('PatchDataImpl', () => { _links: { self: { href: 'dso-href', - } - } + }, + }, }; const operations = [ Object.assign({ op: 'move', from: '/1', - path: '/5' - }) as Operation + path: '/5', + }) as Operation, ]; it('should send a PatchRequest', () => { @@ -224,12 +234,12 @@ describe('PatchDataImpl', () => { dso = Object.assign(new DSpaceObject(), { _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name1 }] + metadata: [{ key: 'dc.title', value: name1 }], }); dso2 = Object.assign(new DSpaceObject(), { _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name2 }] + metadata: [{ key: 'dc.title', value: name2 }], }); spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); diff --git a/src/app/core/data/base/patch-data.ts b/src/app/core/data/base/patch-data.ts index 1f93671458f..adcf98ef947 100644 --- a/src/app/core/data/base/patch-data.ts +++ b/src/app/core/data/base/patch-data.ts @@ -5,22 +5,36 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; +import { + find, + map, + mergeMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../../shared/operators'; +import { ChangeAnalyzer } from '../change-analyzer'; import { RemoteData } from '../remote-data'; -import { find, map, mergeMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { PatchRequest } from '../request.models'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators'; -import { ChangeAnalyzer } from '../change-analyzer'; import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; import { RestRequestMethod } from '../rest-request-method'; -import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service'; - +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './identifiable-data.service'; /** * Interface for a data service that can patch and update objects. diff --git a/src/app/core/data/base/put-data.spec.ts b/src/app/core/data/base/put-data.spec.ts index 6287fe91b13..1430bb31061 100644 --- a/src/app/core/data/base/put-data.spec.ts +++ b/src/app/core/data/base/put-data.spec.ts @@ -6,20 +6,27 @@ * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { PutData, PutDataImpl } from './put-data'; import { RestRequestMethod } from '../rest-request-method'; -import { DSpaceObject } from '../../shared/dspace-object.model'; +import { + PutData, + PutDataImpl, +} from './put-data'; /** * Tests whether calls to `PutData` methods are correctly patched through in a concrete data service that implements it @@ -127,7 +134,7 @@ describe('PutDataImpl', () => { metadata: { // recognized properties will be serialized ['dc.title']: [ { language: 'en', value: 'some object' }, - ] + ], }, data: [ 1, 2, 3, 4 ], // unrecognized properties won't be serialized _links: { self: { href: selfLink } }, @@ -144,7 +151,7 @@ describe('PutDataImpl', () => { method: RestRequestMethod.PUT, body: { // _links are not serialized uuid: obj.uuid, - metadata: obj.metadata + metadata: obj.metadata, }, })); done(); diff --git a/src/app/core/data/base/put-data.ts b/src/app/core/data/base/put-data.ts index 66ae73405eb..e9d2b01eb83 100644 --- a/src/app/core/data/base/put-data.ts +++ b/src/app/core/data/base/put-data.ts @@ -5,18 +5,19 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { PutRequest } from '../request.models'; + import { hasValue } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; +import { GenericConstructor } from '../../shared/generic-constructor'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; +import { PutRequest } from '../request.models'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that can send PUT requests. @@ -55,7 +56,7 @@ export class PutDataImpl extends BaseDataService i */ put(object: T): Observable> { const requestId = this.requestService.generateRequestId(); - const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); + const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor).serialize(object); const request = new PutRequest(requestId, object._links.self.href, serializedObject); if (hasValue(this.responseMsToLive)) { diff --git a/src/app/core/data/base/search-data.spec.ts b/src/app/core/data/base/search-data.spec.ts index 31dddeddfce..af9f87bf2cf 100644 --- a/src/app/core/data/base/search-data.spec.ts +++ b/src/app/core/data/base/search-data.spec.ts @@ -5,12 +5,17 @@ * * http://www.dspace.org/license/ */ -import { constructSearchEndpointDefault, SearchData, SearchDataImpl } from './search-data'; -import { FindListOptions } from '../find-list-options.model'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; + import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../find-list-options.model'; +import { + constructSearchEndpointDefault, + SearchData, + SearchDataImpl, +} from './search-data'; /** * Tests whether calls to `SearchData` methods are correctly patched through in a concrete data service that implements it diff --git a/src/app/core/data/base/search-data.ts b/src/app/core/data/base/search-data.ts index ff0b4929458..f758affa280 100644 --- a/src/app/core/data/base/search-data.ts +++ b/src/app/core/data/base/search-data.ts @@ -5,19 +5,26 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; -import { hasNoValue, isNotEmpty } from '../../../shared/empty.util'; -import { FindListOptions } from '../find-list-options.model'; +import { + filter, + map, +} from 'rxjs/operators'; + +import { + hasNoValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list.model'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Shorthand type for method to construct a search endpoint @@ -112,10 +119,9 @@ export class SearchDataImpl extends BaseDataService[]): Observable { - let result$: Observable; const args = []; - result$ = this.getSearchEndpoint(searchMethod); + const result$ = this.getSearchEndpoint(searchMethod); return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index ccdff75fdb9..95fe5f593fd 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -1,31 +1,41 @@ import { TestBed } from '@angular/core/testing'; -import { BitstreamDataService } from './bitstream-data.service'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { ItemMock } from 'src/app/shared/mocks/item.mock'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, +} from 'src/app/shared/remote-data.utils'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { RequestService } from './request.service'; import { Bitstream } from '../shared/bitstream.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { Observable, of as observableOf } from 'rxjs'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level'; -import { PatchRequest, PutRequest } from './request.models'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; +import { Bundle } from '../shared/bundle.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { BitstreamDataService } from './bitstream-data.service'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import objectContaining = jasmine.objectContaining; import { RemoteData } from './remote-data'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { BundleDataService } from './bundle-data.service'; -import { ItemMock } from 'src/app/shared/mocks/item.mock'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; -import { Bundle } from '../shared/bundle.model'; -import { cold } from 'jasmine-marbles'; +import { + PatchRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; +import objectContaining = jasmine.objectContaining; describe('BitstreamDataService', () => { let service: BitstreamDataService; @@ -41,32 +51,32 @@ describe('BitstreamDataService', () => { id: 'fake-bitstream1', uuid: 'fake-bitstream1', _links: { - self: { href: 'fake-bitstream1-self' } - } + self: { href: 'fake-bitstream1-self' }, + }, }); const bitstream2 = Object.assign(new Bitstream(), { id: 'fake-bitstream2', uuid: 'fake-bitstream2', _links: { - self: { href: 'fake-bitstream2-self' } - } + self: { href: 'fake-bitstream2-self' }, + }, }); const format = Object.assign(new BitstreamFormat(), { id: '2', shortDescription: 'PNG', description: 'Portable Network Graphics', - supportLevel: BitstreamFormatSupportLevel.Known + supportLevel: BitstreamFormatSupportLevel.Known, }); const url = 'fake-bitstream-url'; beforeEach(() => { objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); requestService = getMockRequestService(); halService = Object.assign(new HALEndpointServiceStub(url)); bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { - getBrowseEndpoint: observableOf(bitstreamFormatHref) + getBrowseEndpoint: observableOf(bitstreamFormatHref), }); rdbService = getMockRemoteDataBuildService(); @@ -128,7 +138,7 @@ describe('BitstreamDataService', () => { describe('findPrimaryBitstreamByItemAndName', () => { it('should return primary bitstream', () => { - const exprected$ = cold('(a|)', { a: bitstream1} ); + const exprected$ = cold('(a|)', { a: bitstream1 } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)), }); @@ -137,7 +147,7 @@ describe('BitstreamDataService', () => { }); it('should return null if primary bitstream has not be succeeded ', () => { - const exprected$ = cold('(a|)', { a: null} ); + const exprected$ = cold('(a|)', { a: null } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createFailedRemoteDataObject()), }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 97949ffa25c..81d1d74535b 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,48 +1,74 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable, EMPTY } from 'rxjs'; -import { find, map, switchMap, take } from 'rxjs/operators'; +import { + Operation, + RemoveOperation, +} from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + EMPTY, + Observable, +} from 'rxjs'; +import { + find, + map, + switchMap, + take, +} from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; -import { FollowLinkConfig, followLink } from '../../shared/utils/follow-link-config.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { Bitstream } from '../shared/bitstream.model'; -import { BITSTREAM } from '../shared/bitstream.resource-type'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bundle } from '../shared/bundle.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { BundleDataService } from './bundle-data.service'; -import { buildPaginatedList, PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { PatchRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { NoContent } from '../shared/NoContent.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { PageInfo } from '../shared/page-info.model'; -import { RequestParam } from '../cache/models/request-param.model'; import { sendRequest } from '../shared/request.operators'; -import { FindListOptions } from './find-list-options.model'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { PatchData, PatchDataImpl } from './base/patch-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { + PatchRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NoContent } from '../shared/NoContent.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; -import { Operation, RemoveOperation } from 'fast-json-patch'; -import { getFirstCompletedRemoteData } from '../shared/operators'; /** * A service to retrieve {@link Bitstream}s from the REST API */ -@Injectable({ - providedIn: 'root', -}) -@dataService(BITSTREAM) +@Injectable({ providedIn: 'root' }) export class BitstreamDataService extends IdentifiableDataService implements SearchData, PatchData, DeleteData { private searchData: SearchDataImpl; private patchData: PatchDataImpl; @@ -108,7 +134,7 @@ export class BitstreamDataService extends IdentifiableDataService imp } else { return [bundleRD as any]; } - }) + }), ); } @@ -121,10 +147,10 @@ export class BitstreamDataService extends IdentifiableDataService imp const requestId = this.requestService.generateRequestId(); const bitstreamHref$ = this.getBrowseEndpoint().pipe( map((href: string) => `${href}/${bitstream.id}`), - switchMap((href: string) => this.halService.getEndpoint('format', href)) + switchMap((href: string) => this.halService.getEndpoint('format', href)), ); const formatHref$ = this.bitstreamFormatService.getBrowseEndpoint().pipe( - map((href: string) => `${href}/${format.id}`) + map((href: string) => `${href}/${format.id}`), ); observableCombineLatest([bitstreamHref$, formatHref$]).pipe( map(([bitstreamHref, formatHref]) => { @@ -135,7 +161,7 @@ export class BitstreamDataService extends IdentifiableDataService imp return new PutRequest(requestId, bitstreamHref, formatHref, options); }), sendRequest(this.requestService), - take(1) + take(1), ).subscribe(() => { this.requestService.removeByHrefSubstring(bitstream.self + '/format'); }); @@ -178,7 +204,7 @@ export class BitstreamDataService extends IdentifiableDataService imp const hrefObs = this.getSearchByHref( 'byItemHandle', { searchParams }, - ...linksToFollow + ...linksToFollow, ); return this.findByHref( @@ -227,9 +253,9 @@ export class BitstreamDataService extends IdentifiableDataService imp } return rd.payload.primaryBitstream.pipe( getFirstCompletedRemoteData(), - map((rdb: RemoteData) => rdb.hasSucceeded ? rdb.payload : null) + map((rdb: RemoteData) => rdb.hasSucceeded ? rdb.payload : null), ); - }) + }), ); } diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index 15efebe8c72..234326d453b 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -1,21 +1,36 @@ -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { RestResponse } from '../cache/response.models'; -import { Observable, of as observableOf } from 'rxjs'; -import { Action, Store } from '@ngrx/store'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; import { waitForAsync } from '@angular/core/testing'; -import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { + Action, + Store, +} from '@ngrx/store'; +import { + cold, + getTestScheduler, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction, +} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core-state.model'; -import { RequestEntry } from './request-entry.model'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { RequestEntry } from './request-entry.model'; describe('BitstreamFormatDataService', () => { let service: BitstreamFormatDataService; @@ -31,19 +46,19 @@ describe('BitstreamFormatDataService', () => { const store = { dispatch(action: Action) { // Do Nothing - } + }, } as Store; const requestUUIDs = ['some', 'uuid']; const objectCache = jasmine.createSpyObj('objectCache', { - getByHref: observableOf({ requestUUIDs }) + getByHref: observableOf({ requestUUIDs }), }) as ObjectCacheService; const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', { a: bitstreamFormatsEndpoint }); - } + }, } as HALEndpointService; const notificationsService = {} as NotificationsService; @@ -83,7 +98,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -104,7 +119,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -127,7 +142,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -149,7 +164,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -174,7 +189,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -198,12 +213,12 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); const halService = { getEndpoint(linkPath: string): Observable { return observableOf(bitstreamFormatsEndpoint); - } + }, } as HALEndpointService; service = initTestService(halService); service.clearBitStreamFormatRequests().subscribe(); @@ -222,7 +237,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -245,7 +260,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -268,7 +283,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -289,12 +304,12 @@ describe('BitstreamFormatDataService', () => { getByUUID: hot('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); const halService = { getEndpoint(linkPath: string): Observable { return observableOf(bitstreamFormatsEndpoint); - } + }, } as HALEndpointService; service = initTestService(halService); })); diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 01043898158..97b0fa961a9 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -1,30 +1,50 @@ import { Injectable } from '@angular/core'; -import { createSelector, select, Store } from '@ngrx/store'; +import { + createSelector, + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { + distinctUntilChanged, + map, + tap, +} from 'rxjs/operators'; +import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; + +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction, +} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { coreSelector } from '../core.selectors'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; +import { CoreState } from '../core-state.model'; import { Bitstream } from '../shared/bitstream.model'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteData } from './remote-data'; -import { PostRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; +import { NoContent } from '../shared/NoContent.model'; import { sendRequest } from '../shared/request.operators'; -import { CoreState } from '../core-state.model'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; -import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; -import { NoContent } from '../shared/NoContent.model'; -import { dataService } from './base/data-service.decorator'; +import { RemoteData } from './remote-data'; +import { + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; const bitstreamFormatsStateSelector = createSelector( coreSelector, @@ -38,8 +58,7 @@ const selectedBitstreamFormatSelector = createSelector( /** * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint */ -@Injectable() -@dataService(BITSTREAM_FORMAT) +@Injectable({ providedIn: 'root' }) export class BitstreamFormatDataService extends IdentifiableDataService implements FindAllData, DeleteData { protected linkPath = 'bitstreamformats'; @@ -106,7 +125,7 @@ export class BitstreamFormatDataService extends IdentifiableDataService { return new PostRequest(requestId, endpointURL, bitstreamFormat); }), - sendRequest(this.requestService) + sendRequest(this.requestService), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); @@ -117,7 +136,7 @@ export class BitstreamFormatDataService extends IdentifiableDataService { return this.getBrowseEndpoint().pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) + tap((href: string) => this.requestService.removeByHrefSubstring(href)), ); } diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index 9fa7239ef7c..53d2ec20fc8 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -1,9 +1,9 @@ import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; -import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; +import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type'; +import { BrowseResponseParsingService } from './browse-response-parsing.service'; class TestService extends BrowseResponseParsingService { constructor(protected objectCache: ObjectCacheService) { @@ -26,22 +26,22 @@ describe('BrowseResponseParsingService', () => { describe('', () => { const mockFlatBrowse = { - id: 'title', - browseType: 'flatBrowse', - type: 'browse', - }; + id: 'title', + browseType: 'flatBrowse', + type: 'browse', + }; const mockValueList = { - id: 'author', - browseType: 'valueList', - type: 'browse', - }; + id: 'author', + browseType: 'valueList', + type: 'browse', + }; const mockHierarchicalBrowse = { - id: 'srsc', - browseType: 'hierarchicalBrowse', - type: 'browse', - }; + id: 'srsc', + browseType: 'hierarchicalBrowse', + type: 'browse', + }; it('should deserialize flatBrowses correctly', () => { let deserialized = service.deserialize(mockFlatBrowse); diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts index a568cdb6176..e01fa17f1fb 100644 --- a/src/app/core/data/browse-response-parsing.service.ts +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -1,18 +1,17 @@ import { Injectable } from '@angular/core'; -import { ObjectCacheService } from '../cache/object-cache.service'; + import { hasValue } from '../../shared/empty.util'; -import { - HIERARCHICAL_BROWSE_DEFINITION -} from '../shared/hierarchical-browse-definition.resource-type'; -import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; -import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model'; -import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; -import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Serializer } from '../serializer'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; +import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; +import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; +import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model'; +import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model'; import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; /** * A ResponseParsingService used to parse a REST API response to a BrowseDefinition object diff --git a/src/app/core/data/bundle-data.service.spec.ts b/src/app/core/data/bundle-data.service.spec.ts index e3ba438f9bd..b2c8be06af1 100644 --- a/src/app/core/data/bundle-data.service.spec.ts +++ b/src/app/core/data/bundle-data.service.spec.ts @@ -1,19 +1,23 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { compare, Operation } from 'fast-json-patch'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { Item } from '../shared/item.model'; -import { ChangeAnalyzer } from './change-analyzer'; +import { + compare, + Operation, +} from 'fast-json-patch'; + import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { BundleDataService } from './bundle-data.service'; -import { HALLink } from '../shared/hal-link.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { createPaginatedList } from '../../shared/testing/utils.test'; -import { Bundle } from '../shared/bundle.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core-state.model'; +import { Bundle } from '../shared/bundle.model'; +import { HALLink } from '../shared/hal-link.model'; +import { Item } from '../shared/item.model'; import { testPatchDataImplementation } from './base/patch-data.spec'; +import { BundleDataService } from './bundle-data.service'; +import { ChangeAnalyzer } from './change-analyzer'; class DummyChangeAnalyzer implements ChangeAnalyzer { diff(object1: Item, object2: Item): Operation[] { @@ -41,7 +45,7 @@ describe('BundleDataService', () => { bundleHALLink.href = bundleLink; item = new Item(); item._links = { - bundles: bundleHALLink + bundles: bundleHALLink, }; requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; @@ -56,7 +60,7 @@ describe('BundleDataService', () => { }, getObjectBySelfLink: () => { /* empty */ - } + }, } as any; store = {} as Store; return new BundleDataService( @@ -99,30 +103,30 @@ describe('BundleDataService', () => { metadata: { 'dc.title': [ { - value: 'ORIGINAL' - } - ] - } + value: 'ORIGINAL', + }, + ], + }, }), Object.assign(new Bundle(), { id: 'THUMBNAIL_BUNDLE', metadata: { 'dc.title': [ { - value: 'THUMBNAIL' - } - ] - } + value: 'THUMBNAIL', + }, + ], + }, }), Object.assign(new Bundle(), { id: 'EXTRA_BUNDLE', metadata: { 'dc.title': [ { - value: 'EXTRA' - } - ] - } + value: 'EXTRA', + }, + ], + }, }), ]; spyOn(service, 'findAllByItem').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(bundles))); diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 19f0e737069..5d552c9bf0f 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -1,36 +1,39 @@ import { Injectable } from '@angular/core'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; -import { BUNDLE } from '../shared/bundle.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Bitstream } from '../shared/bitstream.model'; import { RequestEntryState } from './request-entry-state.model'; -import { FindListOptions } from './find-list-options.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RestRequestMethod } from './rest-request-method'; -import { Operation } from 'fast-json-patch'; -import { dataService } from './base/data-service.decorator'; /** * A service to retrieve {@link Bundle}s from the REST API */ -@Injectable( - { providedIn: 'root' }, -) -@dataService(BUNDLE) +@Injectable({ providedIn: 'root' }) export class BundleDataService extends IdentifiableDataService implements PatchData { private bitstreamsEndpoint = 'bitstreams'; @@ -91,7 +94,7 @@ export class BundleDataService extends IdentifiableDataService implement RequestEntryState.Success, null, matchingBundle, - 200 + 200, ); } else { return new RemoteData( @@ -101,7 +104,7 @@ export class BundleDataService extends IdentifiableDataService implement RequestEntryState.Error, `The bundle with name ${bundleName} was not found.`, null, - 404 + 404, ); } } else { @@ -119,7 +122,7 @@ export class BundleDataService extends IdentifiableDataService implement getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable { return this.getBrowseEndpoint().pipe( switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)), - map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href), ); } diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index c1a7ac64c26..431fe941bba 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -1,30 +1,45 @@ -import { CollectionDataService } from './collection-data.service'; -import { RequestService } from './request.service'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; +import { + cold, + getTestScheduler, + hot, +} from 'jasmine-marbles'; +import { Observable } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { hasNoValue } from '../../shared/empty.util'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { ContentSourceRequest, UpdateContentSourceRequest } from './request.models'; -import { ContentSource } from '../shared/content-source.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Collection } from '../shared/collection.model'; +import { ContentSource } from '../shared/content-source.model'; import { PageInfo } from '../shared/page-info.model'; -import { buildPaginatedList } from './paginated-list.model'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { hasNoValue } from '../../shared/empty.util'; import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; import { testFindAllDataImplementation } from './base/find-all-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; -import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { CollectionDataService } from './collection-data.service'; +import { buildPaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { + ContentSourceRequest, + UpdateContentSourceRequest, +} from './request.models'; +import { RequestService } from './request.service'; const url = 'fake-url'; const collectionId = 'fake-collection-id'; @@ -44,9 +59,9 @@ describe('CollectionDataService', () => { name: 'test-collection-1', _links: { self: { - href: 'https://rest.api/collections/test-collection-1-1' - } - } + href: 'https://rest.api/collections/test-collection-1-1', + }, + }, }); const mockCollection2: Collection = Object.assign(new Collection(), { @@ -54,9 +69,9 @@ describe('CollectionDataService', () => { name: 'test-collection-2', _links: { self: { - href: 'https://rest.api/collections/test-collection-2-2' - } - } + href: 'https://rest.api/collections/test-collection-2-2', + }, + }, }); const mockCollection3: Collection = Object.assign(new Collection(), { @@ -64,9 +79,9 @@ describe('CollectionDataService', () => { name: 'test-collection-3', _links: { self: { - href: 'https://rest.api/collections/test-collection-3-3' - } - } + href: 'https://rest.api/collections/test-collection-3-3', + }, + }, }); const queryString = 'test-string'; @@ -139,7 +154,7 @@ describe('CollectionDataService', () => { it('should return a RemoteData> for the getAuthorizedCollection', () => { const result = service.getAuthorizedCollection(queryString); const expected = cold('a|', { - a: paginatedListRD + a: paginatedListRD, }); expect(result).toBeObservable(expected); }); @@ -154,7 +169,7 @@ describe('CollectionDataService', () => { it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { const result = service.getAuthorizedCollectionByCommunity(communityId, queryString); const expected = cold('a|', { - a: paginatedListRD + a: paginatedListRD, }); expect(result).toBeObservable(expected); }); @@ -201,10 +216,10 @@ describe('CollectionDataService', () => { } rdbService = jasmine.createSpyObj('rdbService', { buildList: hot('a|', { - a: paginatedListRD + a: paginatedListRD, }), buildFromRequestUUID: buildResponse$, - buildSingle: buildResponse$ + buildSingle: buildResponse$, }); objectCache = new ObjectCacheServiceStub(); halService = new HALEndpointServiceStub(url); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 405b35c1f94..b2d5476d21a 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -2,41 +2,51 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { INotification } from '../../shared/notifications/models/notification.model'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { Collection } from '../shared/collection.model'; -import { COLLECTION } from '../shared/collection.resource-type'; +import { Community } from '../shared/community.model'; import { ContentSource } from '../shared/content-source.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; +import { + getAllCompletedRemoteData, + getFirstCompletedRemoteData, +} from '../shared/operators'; +import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { ContentSourceRequest, - UpdateContentSourceRequest + UpdateContentSourceRequest, } from './request.models'; import { RequestService } from './request.service'; -import { BitstreamDataService } from './bitstream-data.service'; import { RestRequest } from './rest-request.model'; -import { FindListOptions } from './find-list-options.model'; -import { Community } from '../shared/community.model'; -import { dataService } from './base/data-service.decorator'; -@Injectable() -@dataService(COLLECTION) +@Injectable({ providedIn: 'root' }) export class CollectionDataService extends ComColDataService { protected errorTitle = 'collection.source.update.notifications.error.title'; protected contentSourceError = 'collection.source.update.notifications.error.content'; @@ -73,11 +83,12 @@ export class CollectionDataService extends ComColDataService { getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const searchHref = 'findSubmitAuthorized'; options = Object.assign({}, options, { - searchParams: [new RequestParam('query', query)] + searchParams: [new RequestParam('query', query)], }); return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -102,12 +113,13 @@ export class CollectionDataService extends ComColDataService { options = Object.assign({}, options, { searchParams: [ new RequestParam('query', query), - new RequestParam('entityType', entityType) - ] + new RequestParam('entityType', entityType), + ], }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -121,17 +133,18 @@ export class CollectionDataService extends ComColDataService { * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true,): Observable>> { + getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true): Observable>> { const searchHref = 'findSubmitAuthorizedByCommunity'; options = Object.assign({}, options, { searchParams: [ new RequestParam('uuid', communityId), - new RequestParam('query', query) - ] + new RequestParam('query', query), + ], }); return this.searchBy(searchHref, options, reRequestOnStale).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** * Get all collections the user is authorized to submit to, by community and has the metadata @@ -154,15 +167,16 @@ export class CollectionDataService extends ComColDataService { const searchHref = 'findSubmitAuthorizedByCommunityAndEntityType'; const searchParams = [ new RequestParam('uuid', communityId), - new RequestParam('entityType', entityType) + new RequestParam('entityType', entityType), ]; options = Object.assign({}, options, { - searchParams: searchParams + searchParams: searchParams, }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -177,9 +191,8 @@ export class CollectionDataService extends ComColDataService { options.elementsPerPage = 1; return this.searchBy(searchHref, options).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending), - take(1), - map((collections: RemoteData>) => collections.payload.totalElements > 0) + getFirstCompletedRemoteData(), + map((collections: RemoteData>) => collections?.payload?.totalElements > 0), ); } @@ -189,7 +202,7 @@ export class CollectionDataService extends ComColDataService { */ getHarvesterEndpoint(collectionId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`)) + switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`)), ); } @@ -200,7 +213,7 @@ export class CollectionDataService extends ComColDataService { getContentSource(collectionId: string, useCachedVersionIfAvailable = true): Observable> { const href$ = this.getHarvesterEndpoint(collectionId).pipe( isNotEmptyOperator(), - take(1) + take(1), ); href$.subscribe((href: string) => { @@ -227,7 +240,7 @@ export class CollectionDataService extends ComColDataService { headers = headers.append('Content-Type', 'application/json'); options.headers = headers; return new UpdateContentSourceRequest(requestId, href, JSON.stringify(serializedContentSource), options); - }) + }), ); // Execute the post/put request @@ -255,7 +268,7 @@ export class CollectionDataService extends ComColDataService { return (response as RemoteData).payload; } return response as INotification; - }) + }), ); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 0f9f0fa7402..e1fe48c0762 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,28 +1,37 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { cold } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core-state.model'; +import { Bitstream } from '../shared/bitstream.model'; import { Community } from '../shared/community.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { RequestService } from './request.service'; -import { createFailedRemoteDataObject, createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { BitstreamDataService } from './bitstream-data.service'; -import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; -import { Bitstream } from '../shared/bitstream.model'; -import { testCreateDataImplementation } from './base/create-data.spec'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { RequestService } from './request.service'; const LINK_NAME = 'test'; @@ -45,7 +54,7 @@ class TestService extends ComColDataService { protected http: HttpClient, protected bitstreamDataService: BitstreamDataService, protected comparator: DSOChangeAnalyzer, - protected linkPath: string + protected linkPath: string, ) { super('something', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService); } @@ -79,30 +88,30 @@ describe('ComColDataService', () => { const comparator = {} as any; const options = Object.assign(new FindListOptions(), { - scopeID: scopeID + scopeID: scopeID, }); const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const mockHalService = { - getEndpoint: (linkPath) => observableOf(communitiesEndpoint) + getEndpoint: (linkPath) => observableOf(communitiesEndpoint), }; function initRdbService(): RemoteDataBuildService { return jasmine.createSpyObj('rdbService', { - buildSingle : createFailedRemoteDataObject$('Error', 500) + buildSingle : createFailedRemoteDataObject$('Error', 500), }); } function initBitstreamDataService(): BitstreamDataService { return jasmine.createSpyObj('bitstreamDataService', { - deleteByHref: createSuccessfulRemoteDataObject$({}) + deleteByHref: createSuccessfulRemoteDataObject$({}), }); } function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('cds', { getEndpoint: cold('--a-', { a: communitiesEndpoint }), - getIDHref: communityEndpoint + getIDHref: communityEndpoint, }); } @@ -112,11 +121,11 @@ describe('ComColDataService', () => { d: { _links: { [LINK_NAME]: { - href: scopedEndpoint - } - } - } - }) + href: scopedEndpoint, + }, + }, + }, + }), }); } @@ -132,7 +141,7 @@ describe('ComColDataService', () => { http, bitstreamDataService, comparator, - LINK_NAME + LINK_NAME, ); } @@ -200,12 +209,12 @@ describe('ComColDataService', () => { communityWithParentHref = { _links: { parentCommunity: { - href: 'topLevel/parentCommunity' - } - } + href: 'topLevel/parentCommunity', + }, + }, } as Community; communityWithoutParentHref = { - _links: {} + _links: {}, } as Community; }); @@ -238,9 +247,9 @@ describe('ComColDataService', () => { id: 'a20da287-e174-466a-9926-f66as300d399', metadata: [{ key: 'dc.title', - value: 'parent community' + value: 'parent community', }], - _links: {} + _links: {}, }); }); it('should refresh a specific cached community when the parent link can be resolved', () => { @@ -262,9 +271,9 @@ describe('ComColDataService', () => { dso = { _links: { logo: { - href: 'logo-href' - } - } + href: 'logo-href', + }, + }, }; }); @@ -291,8 +300,8 @@ describe('ComColDataService', () => { _links: { self: { href: 'logo-href', - } - } + }, + }, }); }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index abc9046cd0e..de0d1a31570 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,34 +1,63 @@ -import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { Operation } from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { Community } from '../shared/community.model'; -import { HALLink } from '../shared/hal-link.model'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; import { Bitstream } from '../shared/bitstream.model'; import { Collection } from '../shared/collection.model'; -import { BitstreamDataService } from './bitstream-data.service'; +import { Community } from '../shared/community.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HALLink } from '../shared/hal-link.model'; import { NoContent } from '../shared/NoContent.model'; -import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { FindListOptions } from './find-list-options.model'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { RestRequestMethod } from './rest-request-method'; -import { CreateData, CreateDataImpl } from './base/create-data'; -import { RequestParam } from '../cache/models/request-param.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { BitstreamDataService } from './bitstream-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { Operation } from 'fast-json-patch'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; +import { RestRequestMethod } from './rest-request-method'; export abstract class ComColDataService extends IdentifiableDataService implements CreateData, FindAllData, SearchData, PatchData, DeleteData { private createData: CreateData; @@ -86,7 +115,7 @@ export abstract class ComColDataService extend }), filter((halLink: HALLink) => isNotEmpty(halLink)), map((halLink: HALLink) => halLink.href), - distinctUntilChanged() + distinctUntilChanged(), ); } } @@ -97,7 +126,7 @@ export abstract class ComColDataService extend public findByParent(parentUUID: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { const href$ = this.getFindByParentHref(parentUUID).pipe( - map((href: string) => this.buildHrefFromFindOptions(href, options)) + map((href: string) => this.buildHrefFromFindOptions(href, options)), ); return this.findListByHref(href$, options, true, true, ...linksToFollow); } @@ -110,7 +139,7 @@ export abstract class ComColDataService extend return this.halService.getEndpoint(this.linkPath).pipe( // We can't use HalLinkService to discover the logo link itself, as objects without a logo // don't have the link, and this method is also used in the createLogo method. - map((href: string) => new URLCombiner(href, id, 'logo').toString()) + map((href: string) => new URLCombiner(href, id, 'logo').toString()), ); } @@ -132,7 +161,7 @@ export abstract class ComColDataService extend } else { return this.bitstreamDataService.deleteByHref(logoRd.payload._links.self.href); } - }) + }), ); } else { return createFailedRemoteDataObject$(`The given object doesn't have a logo`, 400); @@ -148,7 +177,7 @@ export abstract class ComColDataService extend this.findByHref(parentCommunityUrl).pipe( getFirstCompletedRemoteData(), ), - this.halService.getEndpoint('communities/search/top').pipe(take(1)) + this.halService.getEndpoint('communities/search/top').pipe(take(1)), ]).subscribe(([rd, topHref]: [RemoteData, string]) => { if (rd.hasSucceeded && isNotEmpty(rd.payload) && isNotEmpty(rd.payload.id)) { this.requestService.setStaleByHrefSubstring(rd.payload.id); diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index efb6d50e848..79dedf0c842 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,26 +1,28 @@ import { Injectable } from '@angular/core'; - import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Community } from '../shared/community.model'; -import { COMMUNITY } from '../shared/community.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -import { BitstreamDataService } from './bitstream-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { FindListOptions } from './find-list-options.model'; -import { dataService } from './base/data-service.decorator'; -@Injectable() -@dataService(COMMUNITY) +@Injectable({ providedIn: 'root' }) export class CommunityDataService extends ComColDataService { protected topLinkPath = 'search/top'; @@ -44,14 +46,14 @@ export class CommunityDataService extends ComColDataService { findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.getEndpoint().pipe( map(href => `${href}/search/top`), - switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow)) + switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow)), ); } protected getFindByParentHref(parentUUID: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( switchMap((communityEndpointHref: string) => - this.halService.getEndpoint('subcommunities', `${communityEndpointHref}/${parentUUID}`)) + this.halService.getEndpoint('subcommunities', `${communityEndpointHref}/${parentUUID}`)), ); } @@ -59,7 +61,7 @@ export class CommunityDataService extends ComColDataService { return this.getEndpoint().pipe( map((endpoint: string) => this.getIDHref(endpoint, options.scopeID)), filter((href: string) => isNotEmpty(href)), - take(1) + take(1), ); } } diff --git a/src/app/core/data/configuration-data.service.spec.ts b/src/app/core/data/configuration-data.service.spec.ts index 7fe69c16e5c..bccfe45da48 100644 --- a/src/app/core/data/configuration-data.service.spec.ts +++ b/src/app/core/data/configuration-data.service.spec.ts @@ -1,12 +1,16 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigurationDataService } from './configuration-data.service'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ConfigurationDataService } from './configuration-data.service'; -import { ConfigurationProperty } from '../shared/configuration-property.model'; describe('ConfigurationDataService', () => { let scheduler: TestScheduler; @@ -18,7 +22,7 @@ describe('ConfigurationDataService', () => { const testObject = { uuid: 'test-property', name: 'test-property', - values: ['value-1', 'value-2'] + values: ['value-1', 'value-2'], } as ConfigurationProperty; const configLink = 'https://rest.api/rest/api/config/properties'; const requestURL = `https://rest.api/rest/api/config/properties/${testObject.name}`; @@ -28,18 +32,18 @@ describe('ConfigurationDataService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: configLink }) + getEndpoint: cold('a', { a: configLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { a: { - payload: testObject - } - }) + payload: testObject, + }, + }), }); objectCache = {} as ObjectCacheService; @@ -70,8 +74,8 @@ describe('ConfigurationDataService', () => { const result = service.findByPropertyName(testObject.name); const expected = cold('a', { a: { - payload: testObject - } + payload: testObject, + }, }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts index 557e13f57ba..bb1bd19ff14 100644 --- a/src/app/core/data/configuration-data.service.ts +++ b/src/app/core/data/configuration-data.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -import { ConfigurationProperty } from '../shared/configuration-property.model'; -import { CONFIG_PROPERTY } from '../shared/config-property.resource-type'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; -@Injectable() -@dataService(CONFIG_PROPERTY) +@Injectable({ providedIn: 'root' }) /** * Data Service responsible for retrieving Configuration properties */ diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index 066ccf28c9d..4c0fd789fbf 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ContentSource } from '../shared/content-source.model'; import { MetadataConfig } from '../shared/metadata-config.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) /** * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a ContentSource object */ diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts index 992a29e4b84..d6aeca7965b 100644 --- a/src/app/core/data/debug-response-parsing.service.ts +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; + import { RestResponse } from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class DebugResponseParsingService implements ResponseParsingService { parse(request: RestRequest, data: RawRestResponse): RestResponse { console.log('request', request, 'data', data); diff --git a/src/app/core/data/default-change-analyzer.service.ts b/src/app/core/data/default-change-analyzer.service.ts index 70c45bbc2de..fa08af018a2 100644 --- a/src/app/core/data/default-change-analyzer.service.ts +++ b/src/app/core/data/default-change-analyzer.service.ts @@ -1,16 +1,19 @@ import { Injectable } from '@angular/core'; -import { compare } from 'fast-json-patch'; -import { Operation } from 'fast-json-patch'; +import { + compare, + Operation, +} from 'fast-json-patch'; + import { getClassForType } from '../cache/builders/build-decorators'; +import { TypedObject } from '../cache/typed-object.model'; import { DSpaceNotNullSerializer } from '../dspace-rest/dspace-not-null.serializer'; import { ChangeAnalyzer } from './change-analyzer'; -import { TypedObject } from '../cache/typed-object.model'; /** * A class to determine what differs between two * CacheableObjects */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DefaultChangeAnalyzer implements ChangeAnalyzer { /** * Compare the metadata of two CacheableObject and return the differences as diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts index a621895633b..95e7b5d69f4 100644 --- a/src/app/core/data/dso-change-analyzer.service.ts +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -1,15 +1,19 @@ -import { compare, Operation } from 'fast-json-patch'; -import { ChangeAnalyzer } from './change-analyzer'; import { Injectable } from '@angular/core'; +import { + compare, + Operation, +} from 'fast-json-patch'; +import cloneDeep from 'lodash/cloneDeep'; + import { DSpaceObject } from '../shared/dspace-object.model'; import { MetadataMap } from '../shared/metadata.models'; -import cloneDeep from 'lodash/cloneDeep'; +import { ChangeAnalyzer } from './change-analyzer'; /** * A class to determine what differs between two * DSpaceObjects */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DSOChangeAnalyzer implements ChangeAnalyzer { /** diff --git a/src/app/core/data/dso-redirect.service.spec.ts b/src/app/core/data/dso-redirect.service.spec.ts index 9271bd5f7f4..b6b72583ea6 100644 --- a/src/app/core/data/dso-redirect.service.spec.ts +++ b/src/app/core/data/dso-redirect.service.spec.ts @@ -1,18 +1,25 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + +import { AppConfig } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment.test'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { DsoRedirectService } from './dso-redirect.service'; -import { GetRequest, IdentifierType } from './request.models'; -import { RequestService } from './request.service'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { Item } from '../shared/item.model'; import { EMBED_SEPARATOR } from './base/base-data.service'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { environment } from '../../../environments/environment.test'; -import { AppConfig } from '../../../config/app-config.interface'; +import { DsoRedirectService } from './dso-redirect.service'; +import { + GetRequest, + IdentifierType, +} from './request.models'; +import { RequestService } from './request.service'; describe('DsoRedirectService', () => { let scheduler: TestScheduler; @@ -35,26 +42,26 @@ describe('DsoRedirectService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: pidLink }) + getEndpoint: cold('a', { a: pidLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); remoteData = createSuccessfulRemoteDataObject(Object.assign(new Item(), { type: 'item', - uuid: '123456789' + uuid: '123456789', })); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { - a: remoteData - }) + a: remoteData, + }), }); redirectService = jasmine.createSpyObj('redirectService', { - redirect: {} + redirect: {}, }); service = new DsoRedirectService( @@ -63,7 +70,7 @@ describe('DsoRedirectService', () => { rdbService, objectCache, halService, - redirectService + redirectService, ); }); @@ -118,8 +125,8 @@ describe('DsoRedirectService', () => { 'dspace.entity.type': [ { language: 'en_US', - value: 'Publication' - } + value: 'Publication', + }, ], }; const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); diff --git a/src/app/core/data/dso-redirect.service.ts b/src/app/core/data/dso-redirect.service.ts index 4585df5b4bd..28628a62464 100644 --- a/src/app/core/data/dso-redirect.service.ts +++ b/src/app/core/data/dso-redirect.service.ts @@ -6,22 +6,29 @@ * http://www.dspace.org/license/ */ /* eslint-disable max-classes-per-file */ -import { Injectable, Inject } from '@angular/core'; +import { + Inject, + Injectable, +} from '@angular/core'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; + +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; +import { getDSORoute } from '../../app-routing-paths'; import { hasValue } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { IdentifierType } from './request.models'; import { RequestService } from './request.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { getDSORoute } from '../../app-routing-paths'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; const ID_ENDPOINT = 'pid'; const UUID_ENDPOINT = 'dso'; @@ -43,7 +50,7 @@ class DsoByIdOrUUIDDataService extends IdentifiableDataService { // interpolate id/uuid as query parameter (endpoint: string, resourceID: string): string => { return endpoint.replace(/{\?id}/, `?id=${resourceID}`) - .replace(/{\?uuid}/, `?uuid=${resourceID}`); + .replace(/{\?uuid}/, `?uuid=${resourceID}`); }, ); } @@ -66,7 +73,7 @@ class DsoByIdOrUUIDDataService extends IdentifiableDataService { * A service to handle redirects from identifier paths to DSO path * e.g.: redirect from /handle/... to /items/... */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DsoRedirectService { private dataService: DsoByIdOrUUIDDataService; @@ -76,7 +83,7 @@ export class DsoRedirectService { protected rdbService: RemoteDataBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - private hardRedirectService: HardRedirectService + private hardRedirectService: HardRedirectService, ) { this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService); } @@ -84,7 +91,7 @@ export class DsoRedirectService { /** * Redirect to a DSpaceObject's path using the given identifier type and ID. * This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]). - * See LookupGuard for more examples. + * See lookupGuard for more examples. * * @param id the identifier of the object to retrieve * @param identifierType the type of the given identifier (defaults to UUID) @@ -97,14 +104,14 @@ export class DsoRedirectService { if (response.hasSucceeded) { const dso = response.payload; if (hasValue(dso.uuid)) { - let newRoute = getDSORoute(dso); + const newRoute = getDSORoute(dso); if (hasValue(newRoute)) { // Use a "301 Moved Permanently" redirect for SEO purposes this.hardRedirectService.redirect(this.appConfig.ui.nameSpace.replace(/\/$/, '') + newRoute, 301); } } } - }) + }), ); } } diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 74117e79d35..5cabba29eb2 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -1,20 +1,25 @@ import { Injectable } from '@angular/core'; +import { + hasNoValue, + hasValue, +} from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { + DSOSuccessResponse, + RestResponse, +} from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; - -import { ResponseParsingService } from './parsing.service'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; /** * @deprecated use DspaceRestResponseParsingService for new code, this is only left to support a * few legacy use cases, and should get removed eventually */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected toCache = true; diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index 0f167ea47e2..1e4809ac4bc 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -1,12 +1,16 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DSpaceObjectDataService } from './dspace-object-data.service'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { DSpaceObjectDataService } from './dspace-object-data.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -16,7 +20,7 @@ describe('DSpaceObjectDataService', () => { let rdbService: RemoteDataBuildService; let objectCache: ObjectCacheService; const testObject = { - uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746' + uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746', } as DSpaceObject; const dsoLink = 'https://rest.api/rest/api/dso/find{?uuid}'; const requestURL = `https://rest.api/rest/api/dso/find?uuid=${testObject.uuid}`; @@ -26,18 +30,18 @@ describe('DSpaceObjectDataService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: dsoLink }) + getEndpoint: cold('a', { a: dsoLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { a: { - payload: testObject - } - }) + payload: testObject, + }, + }), }); objectCache = {} as ObjectCacheService; @@ -68,8 +72,8 @@ describe('DSpaceObjectDataService', () => { const result = service.findById(testObject.uuid); const expected = cold('a', { a: { - payload: testObject - } + payload: testObject, + }, }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 2ad024133c7..bdebbd0582c 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -1,15 +1,13 @@ import { Injectable } from '@angular/core'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; +import { RequestService } from './request.service'; -@Injectable() -@dataService(DSPACE_OBJECT) +@Injectable({ providedIn: 'root' }) export class DSpaceObjectDataService extends IdentifiableDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 500afc4aff6..0177a9813aa 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -1,23 +1,34 @@ /* eslint-disable max-classes-per-file */ -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; -import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { Serializer } from '../serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { PaginatedList, buildPaginatedList } from './paginated-list.model'; -import { getClassForType } from '../cache/builders/build-decorators'; +import { Injectable } from '@angular/core'; + import { environment } from '../../../environments/environment'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ParsedResponse } from '../cache/response.models'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { + getEmbedSizeParams, + getUrlWithoutEmbedParams, +} from '../index/index.selectors'; +import { Serializer } from '../serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; -import { ParsedResponse } from '../cache/response.models'; -import { RestRequestMethod } from './rest-request-method'; -import { getUrlWithoutEmbedParams, getEmbedSizeParams } from '../index/index.selectors'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { PageInfo } from '../shared/page-info.model'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; +import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; +import { RestRequestMethod } from './rest-request-method'; /** @@ -144,8 +155,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService console.warn(`The response for '${request.href}' doesn't have a self link. This could mean there's an issue with the REST endpoint`); response.payload._links = Object.assign({}, response.payload._links, { self: { - href: urlWithoutEmbedParams - } + href: urlWithoutEmbedParams, + }, }); } else { @@ -155,8 +166,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService console.warn(`The response for '${urlWithoutEmbedParams}' has the self link '${response.payload._links.self.href}'. These don't match. This could mean there's an issue with the REST endpoint`); response.payload._links = Object.assign({}, response.payload._links, { self: { - href: urlWithoutEmbedParams - } + href: urlWithoutEmbedParams, + }, }); } } @@ -184,8 +195,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService protected processArray(data: any, request: RestRequest): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { - array = [...array, this.process(datum, request)]; - } + array = [...array, this.process(datum, request)]; + }, ); return array; } @@ -231,7 +242,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService let dataJSON: string; if (hasValue(data._embedded)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts index 728714876c4..c7dd40b98bd 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,17 +1,17 @@ import { Injectable } from '@angular/core'; -import { - DspaceRestResponseParsingService, - isCacheableObject -} from './dspace-rest-response-parsing.service'; +import { environment } from '../../../environments/environment'; import { hasValue } from '../../shared/empty.util'; import { getClassForType } from '../cache/builders/build-decorators'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { CacheableObject } from '../cache/cacheable-object.model'; import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { environment } from '../../../environments/environment'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { + DspaceRestResponseParsingService, + isCacheableObject, +} from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; /** @@ -20,7 +20,7 @@ import { RestRequest } from './rest-request.model'; * * When all endpoints are properly typed, it can be removed. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class EndpointMapResponseParsingService extends DspaceRestResponseParsingService { /** @@ -56,7 +56,7 @@ export class EndpointMapResponseParsingService extends DspaceRestResponseParsing } catch (e) { console.warn(`Couldn't parse endpoint request at ${request.href}`); return new ParsedResponse(response.statusCode, undefined, { - _links: response.payload._links + _links: response.payload._links, }); } } @@ -101,7 +101,7 @@ export class EndpointMapResponseParsingService extends DspaceRestResponseParsing let dataJSON: string; if (hasValue(data._embedded)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts index 4020ff638dd..d47fadce172 100644 --- a/src/app/core/data/entity-type-data.service.ts +++ b/src/app/core/data/entity-type-data.service.ts @@ -1,26 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; -import { RemoteData } from './remote-data'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { + getAllCompletedRemoteData, + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { BaseDataService } from './base/base-data.service'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; -import { ItemType } from '../shared/item-relationships/item-type.model'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { RelationshipTypeDataService } from './relationship-type-data.service'; -import { FindListOptions } from './find-list-options.model'; -import { BaseDataService } from './base/base-data.service'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * Service handling all ItemType requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class EntityTypeDataService extends BaseDataService implements FindAllData, SearchData { private findAllData: FindAllData; private searchData: SearchDataImpl; @@ -48,7 +63,7 @@ export class EntityTypeDataService extends BaseDataService implements */ getRelationshipTypesEndpoint(entityTypeId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`)) + switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`)), ); } @@ -74,8 +89,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedCollection'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -84,7 +98,7 @@ export class EntityTypeDataService extends BaseDataService implements hasMoreThanOneAuthorized(): Observable { const findListOptions: FindListOptions = { elementsPerPage: 2, - currentPage: 1 + currentPage: 1, }; return this.getAllAuthorizedRelationshipType(findListOptions).pipe( map((result: RemoteData>) => { @@ -95,7 +109,7 @@ export class EntityTypeDataService extends BaseDataService implements output = false; } return output; - }) + }), ); } @@ -108,8 +122,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedExternalSource'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -118,18 +131,11 @@ export class EntityTypeDataService extends BaseDataService implements hasMoreThanOneAuthorizedImport(): Observable { const findListOptions: FindListOptions = { elementsPerPage: 2, - currentPage: 1 + currentPage: 1, }; return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( - map((result: RemoteData>) => { - let output: boolean; - if (result.payload) { - output = ( result.payload.page.length > 1 ); - } else { - output = false; - } - return output; - }) + take(1), + map((result: RemoteData>) => result?.payload?.totalElements > 1), ); } diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index afd49271036..a60cef121a8 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -1,16 +1,17 @@ -import { RequestService } from './request.service'; -import { EpersonRegistrationService } from './eperson-registration.service'; -import { RestResponse } from '../cache/response.models'; +import { HttpHeaders } from '@angular/common/http'; import { cold } from 'jasmine-marbles'; -import { PostRequest } from './request.models'; -import { Registration } from '../shared/registration.model'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { RequestEntry } from './request-entry.model'; -import { HttpHeaders } from '@angular/common/http'; + +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RestResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { Registration } from '../shared/registration.model'; +import { EpersonRegistrationService } from './eperson-registration.service'; +import { PostRequest } from './request.models'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; describe('EpersonRegistrationService', () => { let testScheduler; @@ -44,7 +45,7 @@ describe('EpersonRegistrationService', () => { generateRequestId: 'request-id', send: {}, getByUUID: cold('a', - { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }) + { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }), }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: observableOf(rd), @@ -53,7 +54,7 @@ describe('EpersonRegistrationService', () => { service = new EpersonRegistrationService( requestService, rdbService, - halService + halService, ); }); @@ -111,9 +112,9 @@ describe('EpersonRegistrationService', () => { payload: Object.assign(new Registration(), { email: registrationWithUser.email, token: 'test-token', - user: registrationWithUser.user - }) - }) + user: registrationWithUser.user, + }), + }), })); }); @@ -128,10 +129,10 @@ describe('EpersonRegistrationService', () => { jasmine.objectContaining({ uuid: 'request-id', method: 'GET', href: 'rest-url/registrations/search/findByToken?token=test-token', - }), true + }), true, ); expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { - a: 'rest-url/registrations/search/findByToken?token=test-token' + a: 'rest-url/registrations/search/findByToken?token=test-token', }); }); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 499d05af380..90a3fab83a9 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -1,20 +1,33 @@ +import { + HttpHeaders, + HttpParams, +} from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { GetRequest, PostRequest } from './request.models'; import { Observable } from 'rxjs'; -import { filter, find, map } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { Registration } from '../shared/registration.model'; +import { + filter, + find, + map, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; +import { Registration } from '../shared/registration.model'; import { ResponseParsingService } from './parsing.service'; -import { GenericConstructor } from '../shared/generic-constructor'; import { RegistrationResponseParsingService } from './registration-response-parsing.service'; import { RemoteData } from './remote-data'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { HttpHeaders } from '@angular/common/http'; -import { HttpParams } from '@angular/common/http'; +import { + GetRequest, + PostRequest, +} from './request.models'; +import { RequestService } from './request.service'; @Injectable({ providedIn: 'root', @@ -81,11 +94,11 @@ export class EpersonRegistrationService { map((href: string) => { const request = new PostRequest(requestId, href, registration, options); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ); } @@ -105,7 +118,7 @@ export class EpersonRegistrationService { Object.assign(request, { getResponseParser(): GenericConstructor { return RegistrationResponseParsingService; - } + }, }); this.requestService.send(request, true); }); @@ -117,7 +130,7 @@ export class EpersonRegistrationService { } else { return rd; } - }) + }), ); } diff --git a/src/app/core/data/external-source-data.service.spec.ts b/src/app/core/data/external-source-data.service.spec.ts index 723d7f9bed6..5e643cc5491 100644 --- a/src/app/core/data/external-source-data.service.spec.ts +++ b/src/app/core/data/external-source-data.service.spec.ts @@ -1,11 +1,12 @@ -import { ExternalSourceDataService } from './external-source-data.service'; +import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; -import { of as observableOf } from 'rxjs'; -import { GetRequest } from './request.models'; import { testSearchDataImplementation } from './base/search-data.spec'; -import { take } from 'rxjs/operators'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { GetRequest } from './request.models'; describe('ExternalSourceService', () => { let service: ExternalSourceDataService; @@ -22,10 +23,10 @@ describe('ExternalSourceService', () => { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0001' - } - ] - } + value: 'https://orcid.org/0001-0001-0001-0001', + }, + ], + }, }), Object.assign(new ExternalSourceEntry(), { id: '0001-0001-0001-0002', @@ -34,20 +35,20 @@ describe('ExternalSourceService', () => { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0002' - } - ] - } - }) + value: 'https://orcid.org/0001-0001-0001-0002', + }, + ], + }, + }), ]; function init() { requestService = jasmine.createSpyObj('requestService', { generateRequestId: 'request-uuid', - send: {} + send: {}, }); rdbService = jasmine.createSpyObj('rdbService', { - buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)), }); halService = jasmine.createSpyObj('halService', { getEndpoint: observableOf('external-sources-REST-endpoint'), @@ -96,12 +97,6 @@ describe('ExternalSourceService', () => { result.pipe(take(1)).subscribe(); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), false); }); - - it('should return the entries', () => { - result.subscribe((resultRD) => { - expect(resultRD.payload.page).toBe(entries); - }); - }); }); }); }); diff --git a/src/app/core/data/external-source-data.service.ts b/src/app/core/data/external-source-data.service.ts index 02c5e4a53cc..e7f123dd18d 100644 --- a/src/app/core/data/external-source-data.service.ts +++ b/src/app/core/data/external-source-data.service.ts @@ -1,25 +1,37 @@ import { Injectable } from '@angular/core'; -import { ExternalSource } from '../shared/external-source.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; +import { + distinctUntilChanged, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list.model'; -import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { FindListOptions } from './find-list-options.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { SearchData, SearchDataImpl } from './base/search-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * A service handling all external source requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ExternalSourceDataService extends IdentifiableDataService implements SearchData { private searchData: SearchData; @@ -50,7 +62,7 @@ export class ExternalSourceDataService extends IdentifiableDataService { return this.getBrowseEndpoint().pipe( map((href) => this.getIDHref(href, externalSourceId)), - switchMap((href) => this.halService.getEndpoint('entries', href)) + switchMap((href) => this.halService.getEndpoint('entries', href)), ); } @@ -78,7 +90,7 @@ export class ExternalSourceDataService extends IdentifiableDataService { return this.findListByHref(href$, undefined, !hasCachedErrorResponse, reRequestOnStale, ...linksToFollow as any); - }) + }), ) as any; } diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index 3e4493c32bf..4ae22c34d87 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + +import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model'; import { SearchFilterConfig } from '../../shared/search/models/search-filter-config.model'; import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; -import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { @@ -16,19 +17,19 @@ export class FacetConfigResponseParsingService extends DspaceRestResponseParsing const filters = serializer.deserializeArray(config); const _links = { - self: data.payload._links.self + self: data.payload._links.self, }; // fill in the missing links section filters.forEach((filterConfig: SearchFilterConfig) => { _links[filterConfig.name] = { - href: filterConfig._links.self.href + href: filterConfig._links.self.href, }; }); const facetConfigResponse = Object.assign(new FacetConfigResponse(), { filters, - _links + _links, }); this.addToObjectCache(facetConfigResponse, request, data); diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 0911ed50734..5cd24770d8b 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + import { FacetValue } from '../../shared/search/models/facet-value.model'; +import { FacetValues } from '../../shared/search/models/facet-values.model'; import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { FacetValues } from '../../shared/search/models/facet-values.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class FacetValueResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { const payload = data.payload; diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts index ae44d590a4c..4048efe4ff5 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts @@ -1,18 +1,26 @@ -import { AuthorizationDataService } from './authorization-data.service'; -import { SiteDataService } from '../site-data.service'; -import { Site } from '../../shared/site.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { FeatureID } from './feature-id'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; + import { hasValue } from '../../../shared/empty.util'; +import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RequestParam } from '../../cache/models/request-param.model'; +import { EPerson } from '../../eperson/models/eperson.model'; import { Authorization } from '../../shared/authorization.model'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createPaginatedList } from '../../../shared/testing/utils.test'; import { Feature } from '../../shared/feature.model'; -import { FindListOptions } from '../find-list-options.model'; +import { Site } from '../../shared/site.model'; import { testSearchDataImplementation } from '../base/search-data.spec'; -import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { FindListOptions } from '../find-list-options.model'; +import { SiteDataService } from '../site-data.service'; +import { AuthorizationDataService } from './authorization-data.service'; +import { FeatureID } from './feature-id'; describe('AuthorizationDataService', () => { let service: AuthorizationDataService; @@ -23,19 +31,19 @@ describe('AuthorizationDataService', () => { let ePerson: EPerson; const requestService = jasmine.createSpyObj('requestService', { - setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring') + setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring'), }); function init() { site = Object.assign(new Site(), { id: 'test-site', _links: { - self: { href: 'test-site-href' } - } + self: { href: 'test-site-href' }, + }, }); ePerson = Object.assign(new EPerson(), { id: 'test-eperson', - uuid: 'test-eperson' + uuid: 'test-eperson', }); siteService = jasmine.createSpyObj('siteService', { find: observableOf(site), @@ -157,26 +165,26 @@ describe('AuthorizationDataService', () => { const validPayload = [ Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'invalid-feature' - })) + id: 'invalid-feature', + })), }), Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: featureID - })) - }) + id: featureID, + })), + }), ]; const invalidPayload = [ Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'invalid-feature' - })) + id: 'invalid-feature', + })), }), Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'another-invalid-feature' - })) - }) + id: 'another-invalid-feature', + })), + }), ]; const emptyPayload = []; diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index 95730422726..93d8f6b3ecf 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -1,32 +1,47 @@ -import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; -import { AUTHORIZATION } from '../../shared/authorization.resource-type'; -import { Authorization } from '../../shared/authorization.model'; -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + catchError, + map, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { RequestParam } from '../../cache/models/request-param.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { Authorization } from '../../shared/authorization.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { SiteDataService } from '../site-data.service'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { BaseDataService } from '../base/base-data.service'; +import { + SearchData, + SearchDataImpl, +} from '../base/search-data'; +import { FindListOptions } from '../find-list-options.model'; import { PaginatedList } from '../paginated-list.model'; -import { catchError, map, switchMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { RequestParam } from '../../cache/models/request-param.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { SiteDataService } from '../site-data.service'; import { AuthorizationSearchParams } from './authorization-search-params'; import { oneAuthorizationMatchesFeature } from './authorization-utils'; import { FeatureID } from './feature-id'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; -import { FindListOptions } from '../find-list-options.model'; -import { BaseDataService } from '../base/base-data.service'; -import { SearchData, SearchDataImpl } from '../base/search-data'; -import { dataService } from '../base/data-service.decorator'; /** * A service to retrieve {@link Authorization}s from the REST API */ -@Injectable() -@dataService(AUTHORIZATION) +@Injectable({ providedIn: 'root' }) export class AuthorizationDataService extends BaseDataService implements SearchData { protected linkPath = 'authorizations'; protected searchByObjectPath = 'object'; @@ -75,7 +90,7 @@ export class AuthorizationDataService extends BaseDataService imp } }), catchError(() => observableOf([])), - oneAuthorizationMatchesFeature(featureId) + oneAuthorizationMatchesFeature(featureId), ); } @@ -100,7 +115,7 @@ export class AuthorizationDataService extends BaseDataService imp switchMap((url) => { if (hasNoValue(url)) { return this.siteService.find().pipe( - map((site) => site.self) + map((site) => site.self), ); } else { return observableOf(url); @@ -112,7 +127,7 @@ export class AuthorizationDataService extends BaseDataService imp map((url: string) => new AuthorizationSearchParams(url, ePersonUuid, featureId)), switchMap((params: AuthorizationSearchParams) => { return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - }) + }), ); this.addDependency(out$, objectUrl$); diff --git a/src/app/core/data/feature-authorization/authorization-utils.ts b/src/app/core/data/feature-authorization/authorization-utils.ts index a4e5e4d997c..f763b1a38d8 100644 --- a/src/app/core/data/feature-authorization/authorization-utils.ts +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -1,13 +1,25 @@ -import { map, switchMap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { AuthorizationSearchParams } from './authorization-search-params'; -import { SiteDataService } from '../site-data.service'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { AuthService } from '../../auth/auth.service'; import { Authorization } from '../../shared/authorization.model'; import { Feature } from '../../shared/feature.model'; -import { FeatureID } from './feature-id'; import { getFirstSucceededRemoteDataPayload } from '../../shared/operators'; +import { SiteDataService } from '../site-data.service'; +import { AuthorizationSearchParams } from './authorization-search-params'; +import { FeatureID } from './feature-id'; /** * Operator accepting {@link AuthorizationSearchParams} and adding the current {@link Site}'s selflink to the parameter's @@ -20,12 +32,12 @@ export const addSiteObjectUrlIfEmpty = (siteService: SiteDataService) => switchMap((params: AuthorizationSearchParams) => { if (hasNoValue(params.objectUrl)) { return siteService.find().pipe( - map((site) => Object.assign({}, params, { objectUrl: site.self })) + map((site) => Object.assign({}, params, { objectUrl: site.self })), ); } else { return observableOf(params); } - }) + }), ); /** @@ -42,17 +54,17 @@ export const addAuthenticatedUserUuidIfEmpty = (authService: AuthService) => switchMap((authenticated) => { if (authenticated) { return authService.getAuthenticatedUserFromStore().pipe( - map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid })) + map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid })), ); } else { return observableOf(params); } - }) + }), ); } else { return observableOf(params); } - }) + }), ); /** @@ -72,12 +84,12 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) => ...authorizations .filter((authorization: Authorization) => hasValue(authorization.feature)) .map((authorization: Authorization) => authorization.feature.pipe( - getFirstSucceededRemoteDataPayload() - )) + getFirstSucceededRemoteDataPayload(), + )), ]); } else { return observableOf([]); } }), - map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID).length > 0) + map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID).length > 0), ); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts index b41a322cb62..5af9f18b337 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts @@ -1,17 +1,25 @@ import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Collection administrator */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts index 2ab77a00cc4..2092fce1100 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts @@ -1,17 +1,25 @@ import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Community administrator */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 6c1f330c695..2cd9fefa934 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -1,23 +1,41 @@ -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { Observable, of as observableOf } from 'rxjs'; +import { + ActivatedRouteSnapshot, + ResolveFn, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +import { Item } from '../../../shared/item.model'; +import { RemoteData } from '../../remote-data'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; +import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; + +const object = { + self: 'test-selflink', +} as DSpaceObject; + +const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); /** * Test implementation of abstract class DsoPageSingleFeatureGuard */ class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = testResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, protected featureID: FeatureID) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { @@ -30,39 +48,30 @@ describe('DsoPageSingleFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; - let object: DSpaceObject; let route; let parentRoute; function init() { - object = { - self: 'test-selflink' - } as DSpaceObject; - authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} - }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object) + parseUrl: {}, }); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); parentRoute = { params: { - id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' - } + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0', + }, }; route = { params: { }, - parent: parentRoute + parent: parentRoute, }; - guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + guard = new DsoPageSingleFeatureGuardImpl(authorizationService, router, authService, undefined); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts index 3fc90f90696..1f75df846b8 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts @@ -1,9 +1,13 @@ +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { FeatureID } from '../feature-id'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; /** * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts index 071b1b0731f..4e741b5b710 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -1,23 +1,41 @@ -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { Observable, of as observableOf } from 'rxjs'; +import { + ActivatedRouteSnapshot, + ResolveFn, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { Item } from '../../../shared/item.model'; +import { RemoteData } from '../../remote-data'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +const object = { + self: 'test-selflink', +} as DSpaceObject; + +const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); + /** * Test implementation of abstract class DsoPageSomeFeatureGuard */ class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = testResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, protected featureIDs: FeatureID[]) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { @@ -30,39 +48,31 @@ describe('DsoPageSomeFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; - let object: DSpaceObject; + let route; let parentRoute; function init() { - object = { - self: 'test-selflink' - } as DSpaceObject; - authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} - }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object) + parseUrl: {}, }); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); parentRoute = { params: { - id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' - } + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0', + }, }; route = { params: { }, - parent: parentRoute + parent: parentRoute, }; - guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + guard = new DsoPageSomeFeatureGuardImpl(authorizationService, router, authService, []); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index 86837093459..c887b8ae2ab 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -1,12 +1,21 @@ -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { AuthorizationDataService } from '../authorization-data.service'; +import { + ActivatedRouteSnapshot, + ResolveFn, + Router, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { map } from 'rxjs/operators'; -import { DSpaceObject } from '../../../shared/dspace-object.model'; + +import { + hasNoValue, + hasValue, +} from '../../../../shared/empty.util'; import { AuthService } from '../../../auth/auth.service'; -import { hasNoValue, hasValue } from '../../../../shared/empty.util'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; +import { RemoteData } from '../../remote-data'; +import { AuthorizationDataService } from '../authorization-data.service'; import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** @@ -14,8 +23,10 @@ import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guar * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, + + protected abstract resolver: ResolveFn>; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); @@ -26,14 +37,14 @@ export abstract class DsoPageSomeFeatureGuard extends So */ getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const routeWithObjectID = this.getRouteWithDSOId(route); - return (this.resolver.resolve(routeWithObjectID, state) as Observable>).pipe( + return (this.resolver(routeWithObjectID, state) as Observable>).pipe( getAllSucceededRemoteDataPayload(), - map((dso) => dso.self) + map((dso) => dso.self), ); } /** - * Method to resolve resolve (parent) route that contains the UUID of the DSO + * Method to resolve (parent) route that contains the UUID of the DSO * @param route The current route */ protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts index 5afd572326f..5f32e268515 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts @@ -1,17 +1,25 @@ import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index 635aa3530bd..e789f8c4735 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -1,9 +1,17 @@ -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { Observable, of as observableOf } from 'rxjs'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Test implementation of abstract class SingleFeatureAuthorizationGuard @@ -48,13 +56,13 @@ describe('SingleFeatureAuthorizationGuard', () => { ePersonUuid = 'fake-eperson-uuid'; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} + parseUrl: {}, }); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts index cb71d2f4181..cd9f615aa7e 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts @@ -1,7 +1,11 @@ -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { FeatureID } from '../feature-id'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { map} from 'rxjs/operators'; +import { map } from 'rxjs/operators'; + +import { FeatureID } from '../feature-id'; import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index cc6f50c1613..e4f0705defb 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts @@ -1,18 +1,24 @@ import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { FeatureID } from '../feature-id'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { AuthService } from '../../../auth/auth.service'; +import { AuthorizationDataService } from '../authorization-data.service'; +import { FeatureID } from '../feature-id'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator * rights to the {@link Site} */ -@Injectable({ - providedIn: 'root' -}) +@Injectable({ providedIn: 'root' }) export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts index bdbb8250e27..a1a78dc67d6 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts @@ -1,18 +1,24 @@ -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; import { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration * rights to the {@link Site} */ -@Injectable({ - providedIn: 'root' -}) +@Injectable({ providedIn: 'root' }) export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts index 90153d2d148..53d77cadadf 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts @@ -1,8 +1,16 @@ +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { Observable, of as observableOf } from 'rxjs'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** @@ -52,13 +60,13 @@ describe('SomeFeatureAuthorizationGuard', () => { authorizationService = Object.assign({ isAuthorized(featureId?: FeatureID): Observable { return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); - } + }, }); router = jasmine.createSpyObj('router', { - parseUrl: {} + parseUrl: {}, }); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index b909640ea64..229321452f1 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts @@ -1,17 +1,27 @@ -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { FeatureID } from '../feature-id'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; import { switchMap } from 'rxjs/operators'; + import { AuthService } from '../../../auth/auth.service'; import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/authorized.operators'; +import { AuthorizationDataService } from '../authorization-data.service'; +import { FeatureID } from '../feature-id'; /** * Abstract Guard for preventing unauthorized activating and loading of routes when a user * doesn't have authorized rights on any of the specified features and/or object. * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. */ -export abstract class SomeFeatureAuthorizationGuard implements CanActivate { +export abstract class SomeFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { @@ -24,9 +34,9 @@ export abstract class SomeFeatureAuthorizationGuard implements CanActivate { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( switchMap(([featureIDs, objectUrl, ePersonUuid]) => - observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))) + observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))), ), - returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url) + returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url), ); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts index 680495686eb..b301d550a12 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts @@ -1,27 +1,35 @@ import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { + super(authorizationService, router, authService); + } - /** + /** * Check group management rights */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanViewUsageStatistics); - } + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanViewUsageStatistics); + } } diff --git a/src/app/core/data/feature-authorization/feature-data.service.ts b/src/app/core/data/feature-authorization/feature-data.service.ts index eda87911539..191d8b002ca 100644 --- a/src/app/core/data/feature-authorization/feature-data.service.ts +++ b/src/app/core/data/feature-authorization/feature-data.service.ts @@ -1,18 +1,16 @@ import { Injectable } from '@angular/core'; -import { FEATURE } from '../../shared/feature.resource-type'; -import { Feature } from '../../shared/feature.model'; -import { RequestService } from '../request.service'; + import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { Feature } from '../../shared/feature.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { BaseDataService } from '../base/base-data.service'; -import { dataService } from '../base/data-service.decorator'; +import { RequestService } from '../request.service'; /** * A service to retrieve {@link Feature}s from the REST API */ -@Injectable() -@dataService(FEATURE) +@Injectable({ providedIn: 'root' }) export class FeatureDataService extends BaseDataService { protected linkPath = 'features'; diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts index ac0e96a2e6b..6966e6a6311 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts @@ -1,10 +1,10 @@ -import { FilteredDiscoveryPageResponseParsingService } from './filtered-discovery-page-response-parsing.service'; import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; +import { FilteredDiscoveryQueryResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { GenericConstructor } from '../shared/generic-constructor'; +import { FilteredDiscoveryPageResponseParsingService } from './filtered-discovery-page-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { GetRequest } from './request.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { FilteredDiscoveryQueryResponse } from '../cache/response.models'; describe('FilteredDiscoveryPageResponseParsingService', () => { let service: FilteredDiscoveryPageResponseParsingService; @@ -17,15 +17,15 @@ describe('FilteredDiscoveryPageResponseParsingService', () => { const request = Object.assign(new GetRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/path'), { getResponseParser(): GenericConstructor { return FilteredDiscoveryPageResponseParsingService; - } + }, }); const mockResponse = { payload: { - 'discovery-query': 'query' + 'discovery-query': 'query', }, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; it('should return a FilteredDiscoveryQueryResponse containing the correct query', () => { diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts index da7a21c488f..e07f46e9160 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -1,16 +1,20 @@ import { Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; + +import { ObjectCacheService } from '../cache/object-cache.service'; +import { + FilteredDiscoveryQueryResponse, + RestResponse, +} from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { FilteredDiscoveryQueryResponse, RestResponse } from '../cache/response.models'; +import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; /** * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a discovery query (string) * wrapped in a FilteredDiscoveryQueryResponse */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class FilteredDiscoveryPageResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { objectFactory = {}; toCache = false; diff --git a/src/app/core/data/find-list-options.model.ts b/src/app/core/data/find-list-options.model.ts index dc567d4b531..78fe26fcab9 100644 --- a/src/app/core/data/find-list-options.model.ts +++ b/src/app/core/data/find-list-options.model.ts @@ -1,15 +1,15 @@ -import { SortOptions } from '../cache/models/sort-options.model'; import { RequestParam } from '../cache/models/request-param.model'; +import { SortOptions } from '../cache/models/sort-options.model'; /** * The options for a find list request */ export class FindListOptions { - scopeID?: string; - elementsPerPage?: number; - currentPage?: number; - sort?: SortOptions; - searchParams?: RequestParam[]; - startsWith?: string; - fetchThumbnail?: boolean; + scopeID?: string; + elementsPerPage?: number; + currentPage?: number; + sort?: SortOptions; + searchParams?: RequestParam[]; + startsWith?: string; + fetchThumbnail?: boolean; } diff --git a/src/app/core/data/href-only-data.service.spec.ts b/src/app/core/data/href-only-data.service.spec.ts index bf7d2890ea2..cba85d5de65 100644 --- a/src/app/core/data/href-only-data.service.spec.ts +++ b/src/app/core/data/href-only-data.service.spec.ts @@ -1,8 +1,11 @@ -import { HrefOnlyDataService } from './href-only-data.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { FindListOptions } from './find-list-options.model'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; import { BaseDataService } from './base/base-data.service'; +import { FindListOptions } from './find-list-options.model'; +import { HrefOnlyDataService } from './href-only-data.service'; describe(`HrefOnlyDataService`, () => { let service: HrefOnlyDataService; @@ -23,60 +26,60 @@ describe(`HrefOnlyDataService`, () => { expect((service as any).dataService).toBeInstanceOf(BaseDataService); }); - describe(`findByHref`, () => { - beforeEach(() => { - spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); - }); + describe(`findByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); - it(`should forward to findByHref on the internal DataService`, () => { - service.findByHref(href, false, false, ...followLinks); - expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks); - }); + it(`should forward to findByHref on the internal DataService`, () => { + service.findByHref(href, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks); + }); - describe(`when useCachedVersionIfAvailable is omitted`, () => { - it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { - service.findByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything()); - }); + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything()); }); + }); - describe(`when reRequestOnStale is omitted`, () => { - it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => { - service.findByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true); - }); + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true); }); }); + }); - describe(`findListByHref`, () => { - beforeEach(() => { - spy = spyOn((service as any).dataService, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); - }); + describe(`findListByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); - it(`should delegate to findListByHref on the internal DataService`, () => { - service.findListByHref(href, findListOptions, false, false, ...followLinks); - expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks); - }); + it(`should delegate to findListByHref on the internal DataService`, () => { + service.findListByHref(href, findListOptions, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks); + }); - describe(`when findListOptions is omitted`, () => { - it(`should call findListByHref on the internal DataService with findListOptions = {}`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything()); - }); + describe(`when findListOptions is omitted`, () => { + it(`should call findListByHref on the internal DataService with findListOptions = {}`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything()); }); + }); - describe(`when useCachedVersionIfAvailable is omitted`, () => { - it(`should call findListByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything()); - }); + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findListByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything()); }); + }); - describe(`when reRequestOnStale is omitted`, () => { - it(`should call findListByHref on the internal DataService with reRequestOnStale = true`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true); - }); + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findListByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true); }); }); + }); }); diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index 8393d714605..dd40be8f7df 100644 --- a/src/app/core/data/href-only-data.service.ts +++ b/src/app/core/data/href-only-data.service.ts @@ -1,20 +1,17 @@ -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Injectable } from '@angular/core'; -import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { RemoteData } from './remote-data'; import { Observable } from 'rxjs'; -import { PaginatedList } from './paginated-list.model'; -import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; -import { LICENSE } from '../shared/license.resource-type'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CacheableObject } from '../cache/cacheable-object.model'; -import { FindListOptions } from './find-list-options.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { BaseDataService } from './base/base-data.service'; import { HALDataService } from './base/hal-data-service.interface'; -import { dataService } from './base/data-service.decorator'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * A UpdateDataServiceImpl with only findByHref methods. Its purpose is to be used for resources that don't @@ -32,12 +29,7 @@ import { dataService } from './base/data-service.decorator'; * ``` * This means we cannot extend from {@link BaseDataService} directly because the method signatures would not match. */ -@Injectable({ - providedIn: 'root', -}) -@dataService(VOCABULARY_ENTRY) -@dataService(ITEM_TYPE) -@dataService(LICENSE) +@Injectable({ providedIn: 'root' }) export class HrefOnlyDataService implements HALDataService { /** * Works with a {@link BaseDataService} internally, but only exposes two of its methods diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index 03422dadfb0..502b1fe7107 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -1,27 +1,33 @@ -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { + HttpClient, + HttpHeaders, + HttpParams, +} from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { dataService } from './base/data-service.decorator'; +import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core-state.model'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { sendRequest } from '../shared/request.operators'; import { BaseDataService } from './base/base-data.service'; -import { RequestService } from './request.service'; +import { ConfigurationDataService } from './configuration-data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { CoreState } from '../core-state.model'; -import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; -import { Item } from '../shared/item.model'; -import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; -import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { map, switchMap } from 'rxjs/operators'; -import {ConfigurationProperty} from '../shared/configuration-property.model'; -import {ConfigurationDataService} from './configuration-data.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from './request.models'; -import { sendRequest } from '../shared/request.operators'; +import { RequestService } from './request.service'; import { RestRequest } from './rest-request.model'; /** @@ -29,8 +35,7 @@ import { RestRequest } from './rest-request.model'; * from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI' * button appears for admins in the item status page */ -@Injectable() -@dataService(IDENTIFIERS) +@Injectable({ providedIn: 'root' }) export class IdentifierDataService extends BaseDataService { constructor( @@ -61,7 +66,7 @@ export class IdentifierDataService extends BaseDataService { public getIdentifierRegistrationConfiguration(): Observable { return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( getFirstCompletedRemoteData(), - map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) + map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []), ); } @@ -79,7 +84,7 @@ export class IdentifierDataService extends BaseDataService { return new PostRequest(requestId, endpointURL, item._links.self.href, options); }), sendRequest(this.requestService), - switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable>) + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable>), ); } } diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 2c20ed0fb69..dd60d940702 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,25 +1,32 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { BrowseService } from '../browse/browse.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; +import { CoreState } from '../core-state.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { FindListOptions } from './find-list-options.model'; import { ItemDataService } from './item-data.service'; -import { DeleteRequest, PostRequest } from './request.models'; +import { + DeleteRequest, + PostRequest, +} from './request.models'; import { RequestService } from './request.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { CoreState } from '../core-state.model'; import { RequestEntry } from './request-entry.model'; -import { FindListOptions } from './find-list-options.model'; -import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub'; -import { testCreateDataImplementation } from './base/create-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -46,7 +53,7 @@ describe('ItemDataService', () => { const objectCache = {} as ObjectCacheService; const halEndpointService: any = new HALEndpointServiceStub(itemEndpoint); const bundleService = jasmine.createSpyObj('bundleService', { - findByHref: {} + findByHref: {}, }); const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; @@ -54,8 +61,8 @@ describe('ItemDataService', () => { scopeID: scopeID, sort: { field: '', - direction: undefined - } + direction: undefined, + }, }); const browsesEndpoint = 'https://rest.api/discover/browses'; @@ -73,7 +80,7 @@ describe('ItemDataService', () => { cold('--a-', { a: itemBrowseEndpoint }) : cold('--#-', undefined, browseError); return jasmine.createSpyObj('bs', { - getBrowseURLFor: obs + getBrowseURLFor: obs, }); } @@ -158,7 +165,7 @@ describe('ItemDataService', () => { const externalSourceEntry = Object.assign(new ExternalSourceEntry(), { display: 'John, Doe', value: 'John, Doe', - _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } } + _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } }, }); beforeEach(() => { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c3fa84dd6c8..e1f789b5da7 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -8,44 +8,71 @@ /* eslint-disable max-classes-per-file */ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { + distinctUntilChanged, + filter, + find, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { Bundle } from '../shared/bundle.model'; import { Collection } from '../shared/collection.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { GenericConstructor } from '../shared/generic-constructor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { ITEM } from '../shared/item.resource-type'; +import { MetadataMap } from '../shared/metadata.models'; +import { NoContent } from '../shared/NoContent.model'; +import { sendRequest } from '../shared/request.operators'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; +import { ResponseParsingService } from './parsing.service'; import { RemoteData } from './remote-data'; -import { DeleteRequest, GetRequest, PostRequest, PutRequest } from './request.models'; +import { + DeleteRequest, + GetRequest, + PostRequest, + PutRequest, +} from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Bundle } from '../shared/bundle.model'; -import { MetadataMap } from '../shared/metadata.models'; -import { BundleDataService } from './bundle-data.service'; -import { Operation } from 'fast-json-patch'; -import { NoContent } from '../shared/NoContent.model'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { ResponseParsingService } from './parsing.service'; -import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; -import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; -import { FindListOptions } from './find-list-options.model'; -import { ConstructIdEndpoint, IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; import { RestRequestMethod } from './rest-request-method'; -import { CreateData, CreateDataImpl } from './base/create-data'; -import { RequestParam } from '../cache/models/request-param.model'; -import { dataService } from './base/data-service.decorator'; +import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; /** * An abstract service for CRUD operations on Items @@ -140,7 +167,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); }), sendRequest(this.requestService), - switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)) + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), ); } @@ -152,7 +179,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService public setWithDrawn(item: Item, withdrawn: boolean): Observable> { const patchOperation = { - op: 'replace', path: '/withdrawn', value: withdrawn + op: 'replace', path: '/withdrawn', value: withdrawn, } as Operation; this.requestService.removeByHrefSubstring('/discover'); @@ -166,7 +193,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public setDiscoverable(item: Item, discoverable: boolean): Observable> { const patchOperation = { - op: 'replace', path: '/discoverable', value: discoverable + op: 'replace', path: '/discoverable', value: discoverable, } as Operation; this.requestService.removeByHrefSubstring('/discover'); @@ -180,7 +207,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBundlesEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)), ); } @@ -191,10 +218,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBundles(itemId: string, searchOptions?: PaginatedSearchOptions): Observable>> { const hrefObs = this.getBundlesEndpoint(itemId).pipe( - map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href), ); hrefObs.pipe( - take(1) + take(1), ).subscribe((href) => { const request = new GetRequest(this.requestService.generateRequestId(), href); this.requestService.send(request); @@ -215,11 +242,11 @@ export abstract class BaseItemDataService extends IdentifiableDataService const bundleJson = { name: bundleName, - metadata: metadata ? metadata : {} + metadata: metadata ? metadata : {}, }; hrefObs.pipe( - take(1) + take(1), ).subscribe((href) => { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -238,7 +265,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getIdentifiersEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`)), ); } @@ -249,7 +276,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService 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?inheritPolicies=${inheritPolicies}`) + map((endpoint: string) => `${endpoint}/owningCollection?inheritPolicies=${inheritPolicies}`), ); } @@ -275,10 +302,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService // TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code getResponseParser(): GenericConstructor { return StatusCodeOnlyResponseParsingService; - } + }, }); return request; - }) + }), ).subscribe((request) => { this.requestService.send(request); }); @@ -305,7 +332,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService map((href: string) => { const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); @@ -317,7 +344,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBitstreamsEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`)), ); } @@ -403,8 +430,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService /** * A service for CRUD operations on Items */ -@Injectable() -@dataService(ITEM) +@Injectable({ providedIn: 'root' }) export class ItemDataService extends BaseItemDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/data/item-request-data.service.spec.ts b/src/app/core/data/item-request-data.service.spec.ts index a5d18725109..68577ae6e26 100644 --- a/src/app/core/data/item-request-data.service.spec.ts +++ b/src/app/core/data/item-request-data.service.spec.ts @@ -1,12 +1,13 @@ -import { ItemRequestDataService } from './item-request-data.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { of as observableOf } from 'rxjs'; + +import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemRequest } from '../shared/item-request.model'; +import { ItemRequestDataService } from './item-request-data.service'; import { PostRequest } from './request.models'; -import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; describe('ItemRequestDataService', () => { diff --git a/src/app/core/data/item-request-data.service.ts b/src/app/core/data/item-request-data.service.ts index ff6025f7ac8..5c85ed1471d 100644 --- a/src/app/core/data/item-request-data.service.ts +++ b/src/app/core/data/item-request-data.service.ts @@ -1,20 +1,32 @@ +import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + find, + map, +} from 'rxjs/operators'; + +import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { RemoteData } from './remote-data'; -import { PostRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; -import { ItemRequest } from '../shared/item-request.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HttpHeaders } from '@angular/common/http'; -import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemRequest } from '../shared/item-request.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { sendRequest } from '../shared/request.operators'; import { IdentifiableDataService } from './base/identifiable-data.service'; +import { RemoteData } from './remote-data'; +import { + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint @@ -60,11 +72,11 @@ export class ItemRequestDataService extends IdentifiableDataService map((href: string) => { const request = new PostRequest(requestId, href, itemRequest); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ); } diff --git a/src/app/core/data/item-template-data.service.spec.ts b/src/app/core/data/item-template-data.service.spec.ts index 16cf0dbd99d..27db819861c 100644 --- a/src/app/core/data/item-template-data.service.spec.ts +++ b/src/app/core/data/item-template-data.service.spec.ts @@ -1,22 +1,26 @@ -import { ItemTemplateDataService } from './item-template-data.service'; -import { RestResponse } from '../cache/response.models'; -import { RequestService } from './request.service'; -import { Observable, of as observableOf } from 'rxjs'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { Store } from '@ngrx/store'; -import { BrowseService } from '../browse/browse.service'; import { cold } from 'jasmine-marbles'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { CollectionDataService } from './collection-data.service'; -import { RestRequestMethod } from './rest-request-method'; -import { Item } from '../shared/item.model'; -import { RestRequest } from './rest-request.model'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core-state.model'; -import { RequestEntry } from './request-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; import { testCreateDataImplementation } from './base/create-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { CollectionDataService } from './collection-data.service'; +import { ItemTemplateDataService } from './item-template-data.service'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; +import { RestRequest } from './rest-request.model'; +import { RestRequestMethod } from './rest-request-method'; import createSpyObj = jasmine.createSpyObj; describe('ItemTemplateDataService', () => { @@ -46,7 +50,7 @@ describe('ItemTemplateDataService', () => { }, commit(method?: RestRequestMethod) { // Do nothing - } + }, } as RequestService; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; @@ -62,18 +66,18 @@ describe('ItemTemplateDataService', () => { const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', { a: itemEndpoint }); - } + }, } as HALEndpointService; const notificationsService = {} as NotificationsService; const comparator = { diff(first, second) { return [{}]; - } + }, } as any; const collectionService = { getIDHrefObs(id): Observable { return observableOf(collectionEndpoint); - } + }, } as CollectionDataService; function initTestService() { diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index 634c966dbaa..f89a297fad9 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -1,22 +1,23 @@ /* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; -import { BaseItemDataService } from './item-data.service'; -import { Item } from '../shared/item.model'; -import { RemoteData } from './remote-data'; import { Observable } from 'rxjs'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { RequestService } from './request.service'; +import { switchMap } from 'rxjs/operators'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { BrowseService } from '../browse/browse.service'; -import { CollectionDataService } from './collection-data.service'; -import { switchMap } from 'rxjs/operators'; -import { BundleDataService } from './bundle-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; +import { Item } from '../shared/item.model'; import { CreateDataImpl } from './base/create-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { BundleDataService } from './bundle-data.service'; +import { CollectionDataService } from './collection-data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { BaseItemDataService } from './item-data.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * Data service for interacting with Item templates via their Collection @@ -63,7 +64,7 @@ class CollectionItemTemplateDataService extends IdentifiableDataService { /** * A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ItemTemplateDataService extends BaseItemDataService { private byCollection: CollectionItemTemplateDataService; diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index 58598b9870d..93b4ed66968 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -1,18 +1,22 @@ -import { LookupRelationService } from './lookup-relation.service'; -import { ExternalSourceDataService } from './external-source-data.service'; -import { SearchService } from '../shared/search/search.service'; +import { of as observableOf } from 'rxjs'; +import { + skip, + take, +} from 'rxjs/operators'; + +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { buildPaginatedList } from './paginated-list.model'; -import { PageInfo } from '../shared/page-info.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; import { SearchResult } from '../../shared/search/models/search-result.model'; -import { Item } from '../shared/item.model'; -import { skip, take } from 'rxjs/operators'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { ExternalSource } from '../shared/external-source.model'; +import { Item } from '../shared/item.model'; +import { PageInfo } from '../shared/page-info.model'; +import { SearchService } from '../shared/search/search.service'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { LookupRelationService } from './lookup-relation.service'; +import { buildPaginatedList } from './paginated-list.model'; import { RequestService } from './request.service'; -import { of as observableOf } from 'rxjs'; describe('LookupRelationService', () => { let service: LookupRelationService; @@ -24,20 +28,20 @@ describe('LookupRelationService', () => { const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); const relationship = Object.assign(new RelationshipOptions(), { filter: 'test-filter', - configuration: 'test-configuration' + configuration: 'test-configuration', }); const localResults = [ Object.assign(new SearchResult(), { indexableObject: Object.assign(new Item(), { uuid: 'test-item-uuid', - handle: 'test-item-handle' - }) - }) + handle: 'test-item-handle', + }), + }), ]; const externalSource = Object.assign(new ExternalSource(), { id: 'orcidV2', name: 'orcidV2', - hierarchical: false + hierarchical: false, }); const searchServiceEndpoint = 'http://test-rest.com/server/api/core/search'; @@ -47,12 +51,12 @@ describe('LookupRelationService', () => { elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, - currentPage: 1 - }), [{}])) + currentPage: 1, + }), [{}])), }); searchService = jasmine.createSpyObj('searchService', { search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)), - getEndpoint: observableOf(searchServiceEndpoint) + getEndpoint: observableOf(searchServiceEndpoint), }); requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']); service = new LookupRelationService(externalSourceService, searchService, requestService); @@ -77,7 +81,7 @@ describe('LookupRelationService', () => { it('should set the searchConfig to contain a fixedFilter and configuration', () => { expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, - { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }, )); }); }); diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 7a6bc2358b5..55b68afa652 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -1,25 +1,40 @@ -import { ExternalSourceDataService } from './external-source-data.service'; -import { SearchService } from '../shared/search/search.service'; -import { concat, distinctUntilChanged, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { + Observable, + ReplaySubject, +} from 'rxjs'; +import { + concat, + distinctUntilChanged, + map, + multicast, + startWith, + take, + takeWhile, +} from 'rxjs/operators'; + +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Observable, ReplaySubject } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list.model'; import { SearchResult } from '../../shared/search/models/search-result.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; -import { Item } from '../shared/item.model'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { Injectable } from '@angular/core'; import { ExternalSource } from '../shared/external-source.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { Item } from '../shared/item.model'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { SearchService } from '../shared/search/search.service'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; /** * A service for retrieving local and external entries information during a relation lookup */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class LookupRelationService { /** * The search config last used for retrieving local results @@ -31,7 +46,7 @@ export class LookupRelationService { */ private singleResultOptions = Object.assign(new PaginationComponentOptions(), { id: 'single-result-options', - pageSize: 1 + pageSize: 1, }); constructor(protected externalSourceService: ExternalSourceDataService, @@ -47,7 +62,7 @@ export class LookupRelationService { */ getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, - { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }, ); if (setSearchConfig) { this.searchConfig = newConfig; @@ -59,8 +74,8 @@ export class LookupRelationService { () => new ReplaySubject(1), (subject) => subject.pipe( takeWhile((rd: RemoteData>>) => rd.isLoading), - concat(subject.pipe(take(1))) - ) + concat(subject.pipe(take(1))), + ), ) as any , ) as Observable>>>; @@ -76,7 +91,7 @@ export class LookupRelationService { getAllSucceededRemoteData(), getRemoteDataPayload(), map((results: PaginatedList>) => results.totalElements), - startWith(0) + startWith(0), ); } @@ -91,7 +106,7 @@ export class LookupRelationService { getRemoteDataPayload(), map((results: PaginatedList) => results.totalElements), startWith(0), - distinctUntilChanged() + distinctUntilChanged(), ); } diff --git a/src/app/core/data/metadata-field-data.service.spec.ts b/src/app/core/data/metadata-field-data.service.spec.ts index 1ce078f5d53..8d65038060d 100644 --- a/src/app/core/data/metadata-field-data.service.spec.ts +++ b/src/app/core/data/metadata-field-data.service.spec.ts @@ -1,20 +1,21 @@ -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../cache/response.models'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { MetadataFieldDataService } from './metadata-field-data.service'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { RequestParam } from '../cache/models/request-param.model'; -import { FindListOptions } from './find-list-options.model'; -import { createPaginatedList } from '../../shared/testing/utils.test'; +import { RestResponse } from '../cache/response.models'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testCreateDataImplementation } from './base/create-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPutDataImplementation } from './base/put-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPutDataImplementation } from './base/put-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { FindListOptions } from './find-list-options.model'; +import { MetadataFieldDataService } from './metadata-field-data.service'; +import { RequestService } from './request.service'; describe('MetadataFieldDataService', () => { let metadataFieldService: MetadataFieldDataService; @@ -31,8 +32,8 @@ describe('MetadataFieldDataService', () => { prefix: 'dc', namespace: 'namespace', _links: { - self: { href: 'selflink' } - } + self: { href: 'selflink' }, + }, }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', @@ -73,7 +74,7 @@ describe('MetadataFieldDataService', () => { it('should call searchBy with the correct arguments', () => { metadataFieldService.findBySchema(schema); const expectedOptions = Object.assign(new FindListOptions(), { - searchParams: [new RequestParam('schema', schema.prefix)] + searchParams: [new RequestParam('schema', schema.prefix)], }); expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true, true); }); diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index d05e3533d32..aa79ef517ee 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -1,33 +1,43 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { RequestService } from './request.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; +import { RequestParam } from '../cache/models/request-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { RequestParam } from '../cache/models/request-param.model'; -import { FindListOptions } from './find-list-options.model'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { PutData, PutDataImpl } from './base/put-data'; -import { CreateData, CreateDataImpl } from './base/create-data'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; +import { + PutData, + PutDataImpl, +} from './base/put-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint */ -@Injectable() -@dataService(METADATA_FIELD) +@Injectable({ providedIn: 'root' }) export class MetadataFieldDataService extends IdentifiableDataService implements CreateData, PutData, DeleteData, SearchData { private createData: CreateData; private searchData: SearchData; diff --git a/src/app/core/data/metadata-schema-data.service.spec.ts b/src/app/core/data/metadata-schema-data.service.spec.ts index 1bcf4e1104e..02fbc016e7f 100644 --- a/src/app/core/data/metadata-schema-data.service.spec.ts +++ b/src/app/core/data/metadata-schema-data.service.spec.ts @@ -1,16 +1,20 @@ -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { MetadataSchemaDataService } from './metadata-schema-data.service'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../cache/response.models'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { CreateRequest, PutRequest } from './request.models'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { RestResponse } from '../cache/response.models'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { MetadataSchemaDataService } from './metadata-schema-data.service'; +import { + CreateRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; describe('MetadataSchemaDataService', () => { let metadataSchemaService: MetadataSchemaDataService; @@ -61,8 +65,8 @@ describe('MetadataSchemaDataService', () => { prefix: 'dc', namespace: 'namespace', _links: { - self: { href: 'selflink' } - } + self: { href: 'selflink' }, + }, }); }); @@ -78,7 +82,7 @@ describe('MetadataSchemaDataService', () => { describe('called with an existing metadata schema', () => { beforeEach(() => { schema = Object.assign(schema, { - id: 'id-of-existing-schema' + id: 'id-of-existing-schema', }); }); diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 6bd633b8c64..e893cb5404b 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -1,31 +1,41 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; -import { Observable } from 'rxjs'; -import { hasValue } from '../../shared/empty.util'; -import { tap } from 'rxjs/operators'; -import { RemoteData } from './remote-data'; -import { PutData, PutDataImpl } from './base/put-data'; -import { CreateData, CreateDataImpl } from './base/create-data'; import { NoContent } from '../shared/NoContent.model'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PutData, + PutDataImpl, +} from './base/put-data'; import { FindListOptions } from './find-list-options.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { PaginatedList } from './paginated-list.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { dataService } from './base/data-service.decorator'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ -@Injectable() -@dataService(METADATA_SCHEMA) +@Injectable({ providedIn: 'root' }) export class MetadataSchemaDataService extends IdentifiableDataService implements FindAllData, DeleteData { private createData: CreateData; private findAllData: FindAllData; diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts index e46e319149c..f824be7f56b 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -1,21 +1,25 @@ import { Injectable } from '@angular/core'; + +import { hasValue } from '../../shared/empty.util'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; import { ParsedResponse } from '../cache/response.models'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { hasValue } from '../../shared/empty.util'; -import { SearchObjects } from '../../shared/search/models/search-objects.model'; -import { MetadataMap, MetadataValue } from '../shared/metadata.models'; +import { + MetadataMap, + MetadataValue, +} from '../shared/metadata.models'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { // fallback for unexpected empty response const emptyPayload = { _embedded: { - objects: [] - } + objects: [], + }, }; const payload = data.payload._embedded.searchResult || emptyPayload; const hitHighlights: MetadataMap[] = payload._embedded.objects @@ -26,7 +30,7 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer for (const key of Object.keys(hhObject)) { const value: MetadataValue = Object.assign(new MetadataValue(), { value: hhObject[key].join('...'), - language: null + language: null, }); mdMap[key] = [value]; } @@ -46,7 +50,7 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer .map((object, index) => Object.assign({}, object, { indexableObject: dsoSelfLinks[index], hitHighlights: hitHighlights[index], - _embedded: this.filterEmbeddedObjects(object) + _embedded: this.filterEmbeddedObjects(object), })); payload.objects = objects; const deserialized: any = new DSpaceSerializer(SearchObjects).deserialize(payload); @@ -65,8 +69,8 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer .reduce((obj, key) => { obj[key] = object._embedded.indexableObject._embedded[key]; return obj; - }, {}) - }) + }, {}), + }), }); } else { return object; diff --git a/src/app/core/data/notify-services-status-data.service.spec.ts b/src/app/core/data/notify-services-status-data.service.spec.ts new file mode 100644 index 00000000000..e3368435052 --- /dev/null +++ b/src/app/core/data/notify-services-status-data.service.spec.ts @@ -0,0 +1,88 @@ +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotifyRequestsStatusDataService } from './notify-services-status-data.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; +import { RequestEntryState } from './request-entry-state.model'; + +describe('NotifyRequestsStatusDataService test', () => { + let scheduler: TestScheduler; + let service: NotifyRequestsStatusDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + + const endpointURL = `https://rest.api/rest/api/suggestiontargets`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new NotifyRequestsStatusDataService( + requestService, + rdbService, + objectCache, + halService, + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: of(responseCacheEntry), + getByUUID: of(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: of(endpointURL), + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }), + buildFromHref: createSuccessfulRemoteDataObject$({ test: 'test' }), + }); + + + service = initTestService(); + }); + + describe('getNotifyRequestsStatus', () => { + it('should get notify status', (done) => { + service.getNotifyRequestsStatus(requestUUID).subscribe((status) => { + expect(halService.getEndpoint).toHaveBeenCalled(); + expect(requestService.generateRequestId).toHaveBeenCalled(); + expect(status).toEqual(createSuccessfulRemoteDataObject({ test: 'test' } as unknown as NotifyRequestsStatus)); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/notify-services-status-data.service.ts b/src/app/core/data/notify-services-status-data.service.ts index 9c354d08513..deeaad967a7 100644 --- a/src/app/core/data/notify-services-status-data.service.ts +++ b/src/app/core/data/notify-services-status-data.service.ts @@ -1,19 +1,20 @@ import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; +import { + map, + Observable, + take, +} from 'rxjs'; + +import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; -import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model'; -import { NOTIFYREQUEST} from '../../item-page/simple/notify-requests-status/notify-requests-status.resource-type'; -import { Observable, map, take } from 'rxjs'; import { RemoteData } from './remote-data'; import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; - -@Injectable() -@dataService(NOTIFYREQUEST) +@Injectable({ providedIn: 'root' }) export class NotifyRequestsStatusDataService extends IdentifiableDataService { constructor( @@ -21,7 +22,6 @@ export class NotifyRequestsStatusDataService extends IdentifiableDataService + patchOperationService?: GenericConstructor, ) { this.payload = { url, fields, lastModified, patchOperationService }; } @@ -113,7 +114,7 @@ export class SelectVirtualMetadataAction implements Action { uuid: string, select: boolean, ) { - this.payload = { url, source, uuid, select: select}; + this.payload = { url, source, uuid, select: select }; } } @@ -193,7 +194,7 @@ export class DiscardObjectUpdatesAction implements Action { constructor( url: string, notification: INotification, - discardAll = false + discardAll = false, ) { this.payload = { url, notification, discardAll }; } @@ -215,7 +216,7 @@ export class ReinstateObjectUpdatesAction implements Action { * the unique url of the page for which the changes should be reinstated */ constructor( - url: string + url: string, ) { this.payload = { url }; } @@ -237,7 +238,7 @@ export class RemoveObjectUpdatesAction implements Action { * the unique url of the page for which the changes should be removed */ constructor( - url: string + url: string, ) { this.payload = { url }; } @@ -269,7 +270,7 @@ export class RemoveFieldUpdateAction implements Action { */ constructor( url: string, - uuid: string + uuid: string, ) { this.payload = { url, uuid }; } diff --git a/src/app/core/data/object-updates/object-updates.effects.spec.ts b/src/app/core/data/object-updates/object-updates.effects.spec.ts index ffd20a73006..10f37d78cb1 100644 --- a/src/app/core/data/object-updates/object-updates.effects.spec.ts +++ b/src/app/core/data/object-updates/object-updates.effects.spec.ts @@ -1,24 +1,35 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Observable, Subject } from 'rxjs'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { cold, hot } from 'jasmine-marbles'; +import { Action } from '@ngrx/store'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + Subject, +} from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { + INotification, + Notification, +} from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { ObjectUpdatesEffects } from './object-updates.effects'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { DiscardObjectUpdatesAction, ObjectUpdatesAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, - RemoveObjectUpdatesAction + RemoveObjectUpdatesAction, } from './object-updates.actions'; -import { - INotification, - Notification -} from '../../../shared/notifications/models/notification.model'; -import { NotificationType } from '../../../shared/notifications/models/notification-type'; -import { filter } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; -import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { ObjectUpdatesEffects } from './object-updates.effects'; describe('ObjectUpdatesEffects', () => { let updatesEffects: ObjectUpdatesEffects; @@ -31,13 +42,7 @@ describe('ObjectUpdatesEffects', () => { providers: [ ObjectUpdatesEffects, provideMockActions(() => actions), - { - provide: NotificationsService, - useValue: { - remove: (notification) => { /* empty */ - } - } - }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], }); })); @@ -59,7 +64,6 @@ describe('ObjectUpdatesEffects', () => { action = new RemoveObjectUpdatesAction(testURL); }); it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => { - action = new RemoveObjectUpdatesAction(testURL); actions = hot('--a-', { a: action }); (updatesEffects as any).actionMap$[testURL].subscribe((act) => emittedAction = act); const expected = cold('--b-', { b: undefined }); @@ -81,14 +85,19 @@ describe('ObjectUpdatesEffects', () => { removeAction = new RemoveObjectUpdatesAction(testURL); }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a|', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( - filter(((action) => hasValue(action)))) - .subscribe((t) => { - expect(t).toEqual(removeAction); - } - ) - ; + actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('a', { a: undefined })); + + expect(emittedAction).toEqual(removeAction); }); }); @@ -98,12 +107,24 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return an action with type NO_ACTION', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => { - expect(t).toEqual(new NoOpAction()); - } - ); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new ReinstateObjectUpdatesAction(testURL), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( + take(2), + ).subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); + + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); @@ -113,12 +134,22 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new RemoveFieldUpdateAction(testURL, testUUID) }); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new RemoveFieldUpdateAction(testURL, testUUID), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => - expect(t).toEqual(new RemoveObjectUpdatesAction(testURL)) - ); + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); }); diff --git a/src/app/core/data/object-updates/object-updates.effects.ts b/src/app/core/data/object-updates/object-updates.effects.ts index 1dfdc95f23e..5ef86dbbec3 100644 --- a/src/app/core/data/object-updates/object-updates.effects.ts +++ b/src/app/core/data/object-updates/object-updates.effects.ts @@ -1,24 +1,43 @@ import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; import { - DiscardObjectUpdatesAction, - ObjectUpdatesAction, - ObjectUpdatesActionTypes, - RemoveAllObjectUpdatesAction, - RemoveObjectUpdatesAction -} from './object-updates.actions'; -import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; -import { of as observableOf, race as observableRace, Subject } from 'rxjs'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { Action } from '@ngrx/store'; +import { + of as observableOf, + race as observableRace, + Subject, +} from 'rxjs'; +import { + delay, + filter, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { NotificationsActions, NotificationsActionTypes, - RemoveNotificationAction + RemoveNotificationAction, } from '../../../shared/notifications/notifications.actions'; -import { Action } from '@ngrx/store'; -import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + DiscardObjectUpdatesAction, + ObjectUpdatesAction, + ObjectUpdatesActionTypes, + RemoveAllObjectUpdatesAction, + RemoveObjectUpdatesAction, +} from './object-updates.actions'; /** * NGRX effects for ObjectUpdatesActions @@ -52,7 +71,7 @@ export class ObjectUpdatesEffects { /** * Effect that makes sure all last fired ObjectUpdatesActions are stored in the map of this service, with the url as their key */ - mapLastActions$ = createEffect(() => this.actions$ + mapLastActions$ = createEffect(() => this.actions$ .pipe( ofType(...Object.values(ObjectUpdatesActionTypes)), map((action: ObjectUpdatesAction) => { @@ -63,23 +82,23 @@ export class ObjectUpdatesEffects { } this.actionMap$[url].next(action); } - }) + }), ), { dispatch: false }); /** * Effect that makes sure all last fired NotificationActions are stored in the notification map of this service, with the id as their key */ - mapLastNotificationActions$ = createEffect(() => this.actions$ + mapLastNotificationActions$ = createEffect(() => this.actions$ .pipe( ofType(...Object.values(NotificationsActionTypes)), map((action: RemoveNotificationAction) => { - const id: string = action.payload.id || action.payload || this.allIdentifier; - if (hasNoValue(this.notificationActionMap$[id])) { - this.notificationActionMap$[id] = new Subject(); - } - this.notificationActionMap$[id].next(action); + const id: string = action.payload.id || action.payload || this.allIdentifier; + if (hasNoValue(this.notificationActionMap$[id])) { + this.notificationActionMap$[id] = new Subject(); } - ) + this.notificationActionMap$[id].next(action); + }, + ), ), { dispatch: false }); /** @@ -88,52 +107,52 @@ export class ObjectUpdatesEffects { * When a REINSTATE action is fired during the timeout, a NO_ACTION action will be returned * When any other ObjectUpdatesAction is fired during the timeout, a RemoteObjectUpdatesAction will be returned */ - removeAfterDiscardOrReinstateOnUndo$ = createEffect(() => this.actions$ + removeAfterDiscardOrReinstateOnUndo$ = createEffect(() => this.actions$ .pipe( ofType(ObjectUpdatesActionTypes.DISCARD), switchMap((action: DiscardObjectUpdatesAction) => { - const url: string = action.payload.url; - const notification: INotification = action.payload.notification; - const timeOut = notification.options.timeOut; + const url: string = action.payload.url; + const notification: INotification = action.payload.notification; + const timeOut = notification.options.timeOut; - let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); - if (action.payload.discardAll) { - removeAction = new RemoveAllObjectUpdatesAction(); - } - - return observableRace( - // Either wait for the delay and perform a remove action - observableOf(removeAction).pipe(delay(timeOut)), - // Or wait for a a user action - this.actionMap$[url].pipe( - take(1), - tap(() => { - this.notificationsService.remove(notification); - }), - map((updateAction: ObjectUpdatesAction) => { - if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) { - // If someone reinstated, do nothing, just let the reinstating happen - return new NoOpAction(); - } - // If someone performed another action, assume the user does not want to reinstate and remove all changes - return removeAction; - }) - ), - this.notificationActionMap$[notification.id].pipe( - filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), - map(() => { - return removeAction; - }) - ), - this.notificationActionMap$[this.allIdentifier].pipe( - filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), - map(() => { - return removeAction; - }) - ) - ); + let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); + if (action.payload.discardAll) { + removeAction = new RemoveAllObjectUpdatesAction(); } - ) + + return observableRace( + // Either wait for the delay and perform a remove action + observableOf(removeAction).pipe(delay(timeOut)), + // Or wait for a a user action + this.actionMap$[url].pipe( + take(1), + tap(() => { + this.notificationsService.remove(notification); + }), + map((updateAction: ObjectUpdatesAction) => { + if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) { + // If someone reinstated, do nothing, just let the reinstating happen + return new NoOpAction(); + } + // If someone performed another action, assume the user does not want to reinstate and remove all changes + return removeAction; + }), + ), + this.notificationActionMap$[notification.id].pipe( + filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), + map(() => { + return removeAction; + }), + ), + this.notificationActionMap$[this.allIdentifier].pipe( + filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), + map(() => { + return removeAction; + }), + ), + ); + }, + ), )); constructor(private actions$: Actions, diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index 08944a073f7..1f2a15769b4 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -1,5 +1,8 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; + +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { FieldChangeType } from './field-change-type.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -10,11 +13,13 @@ import { RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, - SetValidFieldUpdateAction + SetValidFieldUpdateAction, } from './object-updates.actions'; -import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; -import { FieldChangeType } from './field-change-type.model'; +import { + OBJECT_UPDATES_TRASH_PATH, + objectUpdatesReducer, + ObjectUpdatesState, +} from './object-updates.reducer'; class NullAction extends RemoveFieldUpdateAction { type = null; @@ -29,26 +34,26 @@ const identifiable1 = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320', key: 'dc.contributor.author', language: null, - value: 'Smith, John' + value: 'Smith, John', }; const identifiable1update = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320', key: 'dc.contributor.author', language: null, - value: 'Smith, James' + value: 'Smith, James', }; const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241', key: 'dc.title', language: null, - value: 'New title' + value: 'New title', }; const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e', key: 'dc.description.abstract', language: null, - value: 'Unchanged value' + value: 'Unchanged value', }; const relationship: Relationship = Object.assign(new Relationship(), { uuid: 'test relationship uuid' }); @@ -56,65 +61,62 @@ const modDate = new Date(2010, 2, 11); const uuid = identifiable1.uuid; const url = 'test-object.url/edit'; describe('objectUpdatesReducer', () => { - const testState = { + const testState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: false + isValid: false, }, }, fieldUpdates: { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, - } + }, }; - const discardedTestState = { + const discardedTestState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, }, [url + OBJECT_UPDATES_TRASH_PATH]: { @@ -122,35 +124,32 @@ describe('objectUpdatesReducer', () => { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: false + isValid: false, }, }, fieldUpdates: { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, - } + }, }; deepFreeze(testState); @@ -173,48 +172,80 @@ describe('objectUpdatesReducer', () => { const action = new InitializeFieldsAction(url, [identifiable1, identifiable2], modDate); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SET_EDITABLE_FIELD action without affecting the previous state', () => { const action = new SetEditableFieldUpdateAction(url, uuid, false); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_FIELD action without affecting the previous state', () => { const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the DISCARD action without affecting the previous state', () => { const action = new DiscardObjectUpdatesAction(url, null); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REINSTATE action without affecting the previous state', () => { const action = new ReinstateObjectUpdatesAction(url); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE_FIELD action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => { const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { @@ -226,19 +257,19 @@ describe('objectUpdatesReducer', () => { [identifiable1.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, }, fieldUpdates: {}, virtualMetadataSources: {}, lastModified: modDate, - patchOperationService: undefined - } + patchOperationService: undefined, + }, }; const newState = objectUpdatesReducer(testState, action); expect(newState).toEqual(expectedState); diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 14bacc52db4..e014889850c 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -1,3 +1,14 @@ +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { Item } from '../../shared/item.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { RelationshipType } from '../../shared/item-relationships/relationship-type.model'; +import { FieldChangeType } from './field-change-type.model'; +import { FieldUpdates } from './field-updates.model'; +import { Identifiable } from './identifiable.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -11,15 +22,7 @@ import { SetEditableFieldUpdateAction, SetValidFieldUpdateAction, } from './object-updates.actions'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; -import { Item } from '../../shared/item.model'; -import { RelationshipType } from '../../shared/item-relationships/relationship-type.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { Identifiable } from './identifiable.model'; -import { FieldUpdates } from './field-updates.model'; -import { FieldChangeType } from './field-change-type.model'; /** * Path where discarded objects are saved @@ -77,7 +80,7 @@ export interface DeleteRelationship extends RelationshipIdentifiable { */ export interface ObjectUpdatesEntry { fieldStates: FieldStates; - fieldUpdates: FieldUpdates; + fieldUpdates?: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; patchOperationService?: GenericConstructor; @@ -164,7 +167,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { { fieldUpdates: {} }, { virtualMetadataSources: {} }, { lastModified: lastModifiedServer }, - { patchOperationService } + { patchOperationService }, ); return Object.assign({}, state, { [url]: newPageState }); } @@ -178,7 +181,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) { const url: string = action.payload.url; const field: Identifiable = action.payload.field; const changeType: FieldChangeType = action.payload.changeType; - const pageState: ObjectUpdatesEntry = state[url] || {fieldUpdates: {}}; + const pageState: ObjectUpdatesEntry = state[url] || { fieldUpdates: {} }; let states = pageState.fieldStates; if (changeType === FieldChangeType.ADD) { @@ -231,7 +234,7 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) const newPageState = Object.assign( {}, pageState, - {virtualMetadataSources: virtualMetadataSources}, + { virtualMetadataSources: virtualMetadataSources }, ); return Object.assign( @@ -239,7 +242,7 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) state, { [url]: newPageState, - } + }, ); } @@ -279,7 +282,7 @@ function discardObjectUpdatesFor(url: string, state: any) { const discardedPageState = Object.assign({}, pageState, { fieldUpdates: {}, - fieldStates: newFieldStates + fieldStates: newFieldStates, }); return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); } @@ -357,7 +360,7 @@ function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) { } newPageState = Object.assign({}, state[url], { fieldUpdates: newUpdates, - fieldStates: newFieldStates + fieldStates: newFieldStates, }); } return Object.assign({}, state, { [url]: newPageState }); diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 9cf856f03a0..6602cda080c 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -1,21 +1,23 @@ +import { Injector } from '@angular/core'; import { Store } from '@ngrx/store'; -import { ObjectUpdatesService } from './object-updates.service'; +import { createMockStore } from '@ngrx/store/testing'; +import { of as observableOf } from 'rxjs'; + +import { Notification } from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; +import { CoreState } from '../../core-state.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { FieldChangeType } from './field-change-type.model'; import { DiscardObjectUpdatesAction, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, - SetEditableFieldUpdateAction + SetEditableFieldUpdateAction, } from './object-updates.actions'; -import { of as observableOf } from 'rxjs'; -import { Notification } from '../../../shared/notifications/models/notification.model'; -import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; -import { Injector } from '@angular/core'; -import { FieldChangeType } from './field-change-type.model'; -import { CoreState } from '../../core-state.model'; +import { ObjectUpdatesService } from './object-updates.service'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -31,7 +33,7 @@ describe('ObjectUpdatesService', () => { const fieldUpdates = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, - [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD } + [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, }; const modDate = new Date(2010, 2, 11); @@ -46,15 +48,15 @@ describe('ObjectUpdatesService', () => { }; patchOperationService = jasmine.createSpyObj('patchOperationService', { - fieldUpdatesToPatchOperations: [] + fieldUpdatesToPatchOperations: [], }); const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService, }; - store = new Store(undefined, undefined, undefined); + store = createMockStore({}); spyOn(store, 'dispatch'); injector = jasmine.createSpyObj('injector', { - get: patchOperationService + get: patchOperationService, }); service = new ObjectUpdatesService(store, injector); @@ -80,7 +82,7 @@ describe('ObjectUpdatesService', () => { const expectedResult = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, - [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD } + [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, }; result$.subscribe((result) => { @@ -96,7 +98,7 @@ describe('ObjectUpdatesService', () => { const expectedResult = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, - [identifiable2.uuid]: { field: identifiable2, changeType: undefined } + [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, }; result$.subscribe((result) => { diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 2fb6d47d31c..1f036ddfb1d 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -1,14 +1,36 @@ -import { Injectable, Injector } from '@angular/core'; -import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { coreSelector } from '../../core.selectors'; import { - FieldState, - OBJECT_UPDATES_TRASH_PATH, - ObjectUpdatesEntry, - ObjectUpdatesState, - VirtualMetadataSource -} from './object-updates.reducer'; + Injectable, + Injector, +} from '@angular/core'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, +} from '../../../shared/empty.util'; +import { INotification } from '../../../shared/notifications/models/notification.model'; +import { coreSelector } from '../../core.selectors'; +import { CoreState } from '../../core-state.model'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { FieldChangeType } from './field-change-type.model'; +import { FieldUpdates } from './field-updates.model'; +import { Identifiable } from './identifiable.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -17,24 +39,16 @@ import { RemoveFieldUpdateAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, - SetValidFieldUpdateAction + SetValidFieldUpdateAction, } from './object-updates.actions'; -import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { - hasNoValue, - hasValue, - isEmpty, - isNotEmpty, - hasValueOperator -} from '../../../shared/empty.util'; -import { INotification } from '../../../shared/notifications/models/notification.model'; -import { Operation } from 'fast-json-patch'; + FieldState, + OBJECT_UPDATES_TRASH_PATH, + ObjectUpdatesEntry, + ObjectUpdatesState, + VirtualMetadataSource, +} from './object-updates.reducer'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { Identifiable } from './identifiable.model'; -import { FieldUpdates } from './field-updates.model'; -import { FieldChangeType } from './field-change-type.model'; -import { CoreState } from '../../core-state.model'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -55,7 +69,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel /** * Service that dispatches and reads from the ObjectUpdates' state in the store */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ObjectUpdatesService { constructor(private store: Store, private injector: Injector) { @@ -122,7 +136,7 @@ export class ObjectUpdatesService { fieldUpdates[uuid] = fieldUpdatesExclusive[uuid]; }); return fieldUpdates; - }) + }), ); }), ); @@ -139,16 +153,16 @@ export class ObjectUpdatesService { return objectUpdates.pipe( hasValueOperator(), map((objectEntry) => { - const fieldUpdates: FieldUpdates = {}; - for (const object of initialFields) { - let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; - if (isEmpty(fieldUpdate)) { - fieldUpdate = { field: object, changeType: undefined }; + const fieldUpdates: FieldUpdates = {}; + for (const object of initialFields) { + let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; + if (isEmpty(fieldUpdate)) { + fieldUpdate = { field: object, changeType: undefined }; + } + fieldUpdates[object.uuid] = fieldUpdate; } - fieldUpdates[object.uuid] = fieldUpdate; - } - return fieldUpdates; - })); + return fieldUpdates; + })); } /** @@ -161,7 +175,7 @@ export class ObjectUpdatesService { return fieldState$.pipe( filter((fieldState) => hasValue(fieldState)), map((fieldState) => fieldState.editable), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -175,7 +189,7 @@ export class ObjectUpdatesService { return fieldState$.pipe( filter((fieldState) => hasValue(fieldState)), map((fieldState) => fieldState.isValid), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -189,7 +203,7 @@ export class ObjectUpdatesService { map((entry: ObjectUpdatesEntry) => { return Object.values(entry.fieldStates).findIndex((state: FieldState) => !state.isValid) < 0; }), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -234,7 +248,7 @@ export class ObjectUpdatesService { .pipe( select(virtualMetadataSourceSelector(url, relationship)), map((virtualMetadataSource) => virtualMetadataSource && virtualMetadataSource[item]), - ); + ); } /** @@ -367,7 +381,7 @@ export class ObjectUpdatesService { patch = this.injector.get(entry.patchOperationService).fieldUpdatesToPatchOperations(entry.fieldUpdates); } return patch; - }) + }), ); } } diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts index db46426b79f..ddf38dd2bb1 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts @@ -1,8 +1,9 @@ -import { MetadataPatchOperationService } from './metadata-patch-operation.service'; import { Operation } from 'fast-json-patch'; + import { MetadatumViewModel } from '../../../shared/metadata.models'; -import { FieldUpdates } from '../field-updates.model'; import { FieldChangeType } from '../field-change-type.model'; +import { FieldUpdates } from '../field-updates.model'; +import { MetadataPatchOperationService } from './metadata-patch-operation.service'; describe('MetadataPatchOperationService', () => { let service: MetadataPatchOperationService; @@ -23,13 +24,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -46,13 +47,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Added title', - place: 0 + place: 0, }), - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }); expected = [ - { op: 'add', path: '/metadata/dc.title/-', value: [{ value: 'Added title', language: undefined }] } + { op: 'add', path: '/metadata/dc.title/-', value: [{ value: 'Added title', language: undefined }] }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -69,13 +70,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Changed title', - place: 0 + place: 0, }), - changeType: FieldChangeType.UPDATE - } + changeType: FieldChangeType.UPDATE, + }, }); expected = [ - { op: 'replace', path: '/metadata/dc.title/0', value: { value: 'Changed title', language: undefined } } + { op: 'replace', path: '/metadata/dc.title/0', value: { value: 'Changed title', language: undefined } }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -92,31 +93,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/0' }, { op: 'remove', path: '/metadata/dc.title/0' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -133,31 +134,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/2' }, { op: 'remove', path: '/metadata/dc.title/1' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -174,31 +175,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/1' }, { op: 'remove', path: '/metadata/dc.title/1' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -215,31 +216,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third changed title', - place: 2 + place: 2, }), - changeType: FieldChangeType.UPDATE + changeType: FieldChangeType.UPDATE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/1' }, { op: 'replace', path: '/metadata/dc.title/1', value: { value: 'Third changed title', language: undefined } }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts index 33e9129a9d1..b6dccb759b2 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts @@ -1,21 +1,22 @@ -import { PatchOperationService } from './patch-operation.service'; -import { MetadatumViewModel } from '../../../shared/metadata.models'; -import { Operation } from 'fast-json-patch'; import { Injectable } from '@angular/core'; -import { MetadataPatchOperation } from './operations/metadata/metadata-patch-operation.model'; +import { Operation } from 'fast-json-patch'; + import { hasValue } from '../../../../shared/empty.util'; +import { MetadatumViewModel } from '../../../shared/metadata.models'; +import { FieldChangeType } from '../field-change-type.model'; +import { FieldUpdates } from '../field-updates.model'; import { MetadataPatchAddOperation } from './operations/metadata/metadata-patch-add-operation.model'; +import { MetadataPatchOperation } from './operations/metadata/metadata-patch-operation.model'; import { MetadataPatchRemoveOperation } from './operations/metadata/metadata-patch-remove-operation.model'; import { MetadataPatchReplaceOperation } from './operations/metadata/metadata-patch-replace-operation.model'; -import { FieldUpdates } from '../field-updates.model'; -import { FieldChangeType } from '../field-change-type.model'; +import { PatchOperationService } from './patch-operation.service'; /** * Service transforming {@link FieldUpdates} into {@link Operation}s for metadata values * This expects the fields within every {@link FieldUpdate} to be {@link MetadatumViewModel}s */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MetadataPatchOperationService implements PatchOperationService { @@ -75,7 +76,7 @@ export class MetadataPatchOperationService implements PatchOperationService { const metadatum = update.field as MetadatumViewModel; const val = { value: metadatum.value, - language: metadatum.language + language: metadatum.language, }; let operation: MetadataPatchOperation; diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts index 7f9b1d772f4..9242290c6b7 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch add Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts index 962d53dfee4..d80ec16cd1a 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch move Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts index 61fbae1980b..efaf61f3814 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch remove Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts index e889bede0b7..c2d95812933 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch replace Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts index 171c1d2a54e..7e9c1087ff0 100644 --- a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts @@ -1,4 +1,5 @@ import { Operation } from 'fast-json-patch'; + import { FieldUpdates } from '../field-updates.model'; /** diff --git a/src/app/core/data/paginated-list.model.ts b/src/app/core/data/paginated-list.model.ts index 415bfe234ea..e412af49863 100644 --- a/src/app/core/data/paginated-list.model.ts +++ b/src/app/core/data/paginated-list.model.ts @@ -1,13 +1,22 @@ -import { PageInfo } from '../shared/page-info.model'; -import { hasValue, isEmpty, hasNoValue, isUndefined } from '../../shared/empty.util'; -import { HALResource } from '../shared/hal-resource.model'; -import { HALLink } from '../shared/hal-link.model'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + +import { + hasNoValue, + hasValue, + isEmpty, + isUndefined, +} from '../../shared/empty.util'; import { typedObject } from '../cache/builders/build-decorators'; -import { PAGINATED_LIST } from './paginated-list.resource-type'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { PageInfo } from '../shared/page-info.model'; import { ResourceType } from '../shared/resource-type'; import { excludeFromEquals } from '../utilities/equals.decorators'; -import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { PAGINATED_LIST } from './paginated-list.resource-type'; /** * Factory function for a paginated list @@ -45,7 +54,7 @@ export const buildPaginatedList = (pageInfo: PageInfo, page: T[], normalized } result._links = Object.assign({}, _links, pageInfo._links, { - page: pageLinks + page: pageLinks, }); if (!normalized || isUndefined(pageLinks)) { @@ -64,13 +73,13 @@ export class PaginatedList extends CacheableObject { * The type of the list */ @excludeFromEquals - type = PAGINATED_LIST; + type = PAGINATED_LIST; /** * The type of objects in the list */ @autoserialize - objectType?: ResourceType; + objectType?: ResourceType; /** * The list of objects that represents the current page @@ -81,13 +90,13 @@ export class PaginatedList extends CacheableObject { * the {@link PageInfo} object */ @autoserialize - pageInfo?: PageInfo; + pageInfo?: PageInfo; /** * The {@link HALLink}s for this PaginatedList */ @deserialize - _links: { + _links: { self: HALLink; page: HALLink[]; first?: HALLink; diff --git a/src/app/core/data/parsing.service.ts b/src/app/core/data/parsing.service.ts index fbebe75b2b5..9bf91121cc4 100644 --- a/src/app/core/data/parsing.service.ts +++ b/src/app/core/data/parsing.service.ts @@ -1,5 +1,5 @@ -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RestRequest } from './rest-request.model'; export interface ResponseParsingService { diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts index 00d6d7f03ce..6a9c89f7968 100644 --- a/src/app/core/data/primary-bitstream.service.spec.ts +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -1,20 +1,30 @@ -import { ObjectCacheService } from '../cache/object-cache.service'; -import { RequestService } from './request.service'; -import { Bitstream } from '../shared/bitstream.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { PrimaryBitstreamService } from './primary-bitstream.service'; -import { BundleDataService } from './bundle-data.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { CreateRequest, DeleteRequest, PostRequest, PutRequest } from './request.models'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; -import { getTestScheduler } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { BundleDataService } from './bundle-data.service'; +import { PrimaryBitstreamService } from './primary-bitstream.service'; +import { + CreateRequest, + DeleteRequest, + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; describe('PrimaryBitstreamService', () => { let service: PrimaryBitstreamService; @@ -28,8 +38,8 @@ describe('PrimaryBitstreamService', () => { const bitstream = Object.assign(new Bitstream(), { uuid: 'fake-bitstream', _links: { - self: { href: 'fake-bitstream-self' } - } + self: { href: 'fake-bitstream-self' }, + }, }); const bundle = Object.assign(new Bundle(), { @@ -37,21 +47,21 @@ describe('PrimaryBitstreamService', () => { _links: { self: { href: 'fake-bundle-self' }, primaryBitstream: { href: 'fake-primary-bitstream-self' }, - } + }, }); const url = 'fake-bitstream-url'; beforeEach(() => { objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); requestService = getMockRequestService(); halService = Object.assign(new HALEndpointServiceStub(url)); rdbService = getMockRemoteDataBuildService(); notificationService = new NotificationsServiceStub() as any; - bundleDataService = jasmine.createSpyObj('bundleDataService', {'findByHref': createSuccessfulRemoteDataObject$(bundle)}); + bundleDataService = jasmine.createSpyObj('bundleDataService', { 'findByHref': createSuccessfulRemoteDataObject$(bundle) }); service = new PrimaryBitstreamService(requestService, rdbService, objectCache, halService, notificationService, bundleDataService); }); @@ -96,7 +106,7 @@ describe('PrimaryBitstreamService', () => { expect((service as any).createAndSendRequest).toHaveBeenCalledWith( PostRequest, bundle._links.primaryBitstream.href, - bitstream.self + bitstream.self, ); }); }); @@ -113,7 +123,7 @@ describe('PrimaryBitstreamService', () => { expect((service as any).createAndSendRequest).toHaveBeenCalledWith( PutRequest, bundle._links.primaryBitstream.href, - bitstream.self + bitstream.self, ); }); }); @@ -121,12 +131,12 @@ describe('PrimaryBitstreamService', () => { const testBundle = Object.assign(new Bundle(), { _links: { self: { - href: 'test-href' + href: 'test-href', }, primaryBitstream: { - href: 'test-primaryBitstream-href' - } - } + href: 'test-primaryBitstream-href', + }, + }, }); describe('when the delete request succeeds', () => { diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 488cb5d22e7..a5367e67ed4 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -1,20 +1,28 @@ -import { Bitstream } from '../shared/bitstream.model'; +import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; +import { + Observable, + switchMap, +} from 'rxjs'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable, switchMap } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { Bundle } from '../shared/bundle.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { HttpHeaders } from '@angular/common/http'; +import { Bitstream } from '../shared/bitstream.model'; +import { Bundle } from '../shared/bundle.model'; import { GenericConstructor } from '../shared/generic-constructor'; -import { PutRequest, PostRequest, DeleteRequest } from './request.models'; -import { getAllCompletedRemoteData } from '../shared/operators'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; +import { getAllCompletedRemoteData } from '../shared/operators'; import { BundleDataService } from './bundle-data.service'; +import { RemoteData } from './remote-data'; +import { + DeleteRequest, + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; @Injectable({ providedIn: 'root', @@ -63,8 +71,8 @@ export class PrimaryBitstreamService { requestId, endpointURL, primaryBitstreamSelfLink, - this.getHttpOptions() - ); + this.getHttpOptions(), + ); this.requestService.send(request); @@ -81,7 +89,7 @@ export class PrimaryBitstreamService { return this.createAndSendRequest( PostRequest, bundle._links.primaryBitstream.href, - primaryBitstream.self + primaryBitstream.self, ) as Observable>; } @@ -95,7 +103,7 @@ export class PrimaryBitstreamService { return this.createAndSendRequest( PutRequest, bundle._links.primaryBitstream.href, - primaryBitstream.self + primaryBitstream.self, ) as Observable>; } @@ -107,12 +115,12 @@ export class PrimaryBitstreamService { delete(bundle: Bundle): Observable> { return this.createAndSendRequest( DeleteRequest, - bundle._links.primaryBitstream.href + bundle._links.primaryBitstream.href, ).pipe( getAllCompletedRemoteData(), switchMap((rd: RemoteData) => { return this.bundleDataService.findByHref(bundle.self, rd.hasFailed); - }) + }), ); } diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index 99cd317cdb2..217567776c2 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -6,33 +6,42 @@ * http://www.dspace.org/license/ */ -import { testFindAllDataImplementation } from '../base/find-all-data.spec'; -import { ProcessDataService, TIMER_FACTORY } from './process-data.service'; -import { testDeleteDataImplementation } from '../base/delete-data.spec'; -import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { RequestService } from '../request.service'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; +import { + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { ReducerManager } from '@ngrx/store'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + import { Process } from '../../../process-page/processes/process.model'; import { ProcessStatus } from '../../../process-page/processes/process-status.model'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { ReducerManager } from '@ngrx/store'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; -import { BitstreamFormatDataService } from '../bitstream-format-data.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TestScheduler } from 'rxjs/testing'; +import { testDeleteDataImplementation } from '../base/delete-data.spec'; +import { testFindAllDataImplementation } from '../base/find-all-data.spec'; import { testSearchDataImplementation } from '../base/search-data.spec'; -import { PaginatedList } from '../paginated-list.model'; +import { BitstreamFormatDataService } from '../bitstream-format-data.service'; +import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; import { FindListOptions } from '../find-list-options.model'; -import { of } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { RequestEntryState } from '../request-entry-state.model'; +import { + ProcessDataService, + TIMER_FACTORY, +} from './process-data.service'; describe('ProcessDataService', () => { let testScheduler; - const mockTimer = (fn: () => {}, interval: number) => { + const mockTimer = (fn: () => any, interval: number) => { fn(); return 555; }; @@ -66,7 +75,7 @@ describe('ProcessDataService', () => { { provide: BitstreamFormatDataService, useValue: null }, { provide: NotificationsService, useValue: null }, { provide: TIMER_FACTORY, useValue: mockTimer }, - ] + ], }); processDataService = TestBed.inject(ProcessDataService); @@ -82,13 +91,13 @@ describe('ProcessDataService', () => { spyOn(processDataService, 'findById').and.returnValue( cold('c', { - 'c': completedProcessRD - }) + 'c': completedProcessRD, + }), ); let process$ = processDataService.autoRefreshUntilCompletion('instantly'); expectObservable(process$).toBe('c', { - c: completedProcessRD + c: completedProcessRD, }); }); @@ -101,9 +110,9 @@ describe('ProcessDataService', () => { const runningProcess = Object.assign(new Process(), { _links: { self: { - href: 'https://rest.api/processes/123' - } - } + href: 'https://rest.api/processes/123', + }, + }, }); runningProcess.processStatus = ProcessStatus.RUNNING; const completedProcess = new Process(); @@ -114,14 +123,14 @@ describe('ProcessDataService', () => { spyOn(processDataService, 'findById').and.returnValue( cold('r 150ms c', { 'r': runningProcessRD, - 'c': completedProcessRD - }) + 'c': completedProcessRD, + }), ); let process$ = processDataService.autoRefreshUntilCompletion('foo', 100); expectObservable(process$).toBe('r 150ms c', { 'r': runningProcessRD, - 'c': completedProcessRD + 'c': completedProcessRD, }); }); @@ -146,7 +155,7 @@ describe('ProcessDataService', () => { { provide: BitstreamFormatDataService, useValue: null }, { provide: NotificationsService, useValue: null }, { provide: TIMER_FACTORY, useValue: mockTimer }, - ] + ], }); processDataService = TestBed.inject(ProcessDataService); @@ -156,9 +165,9 @@ describe('ProcessDataService', () => { const runningProcess = Object.assign(new Process(), { _links: { self: { - href: 'https://rest.api/processes/123' - } - } + href: 'https://rest.api/processes/123', + }, + }, }); runningProcess.processStatus = ProcessStatus.RUNNING; @@ -166,15 +175,15 @@ describe('ProcessDataService', () => { page: [runningProcess], _links: { self: { - href: 'https://rest.api/processesList/456' - } - } + href: 'https://rest.api/processesList/456', + }, + }, }); const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcessPagination); spyOn(processDataService, 'searchBy').and.returnValue( - of(runningProcessRD) + of(runningProcessRD), ); expect(processDataService.searchBy).toHaveBeenCalledTimes(0); diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 080a4a4c09f..b39a500c804 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -1,28 +1,49 @@ -import { Injectable, NgZone, Inject, InjectionToken } from '@angular/core'; -import { RequestService } from '../request.service'; +import { + Inject, + Injectable, + InjectionToken, + NgZone, +} from '@angular/core'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + find, + switchMap, +} from 'rxjs/operators'; +import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; + +import { Process } from '../../../process-page/processes/process.model'; +import { hasValue } from '../../../shared/empty.util'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { Process } from '../../../process-page/processes/process.model'; -import { PROCESS } from '../../../process-page/processes/process.resource-type'; -import { Observable, Subscription } from 'rxjs'; -import { switchMap, filter, distinctUntilChanged, find } from 'rxjs/operators'; -import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; -import { RemoteData } from '../remote-data'; -import { BitstreamDataService } from '../bitstream-data.service'; -import { IdentifiableDataService } from '../base/identifiable-data.service'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; -import { FindListOptions } from '../find-list-options.model'; -import { dataService } from '../base/data-service.decorator'; -import { DeleteData, DeleteDataImpl } from '../base/delete-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { NoContent } from '../../shared/NoContent.model'; import { getAllCompletedRemoteData } from '../../shared/operators'; -import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; -import { hasValue } from '../../../shared/empty.util'; -import { SearchData, SearchDataImpl } from '../base/search-data'; +import { + DeleteData, + DeleteDataImpl, +} from '../base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from '../base/find-all-data'; +import { IdentifiableDataService } from '../base/identifiable-data.service'; +import { + SearchData, + SearchDataImpl, +} from '../base/search-data'; +import { BitstreamDataService } from '../bitstream-data.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; /** * Create an InjectionToken for the default JS setTimeout function, purely so we can mock it during @@ -30,13 +51,11 @@ import { SearchData, SearchDataImpl } from '../base/search-data'; */ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout>('timer', { providedIn: 'root', - factory: () => setTimeout + factory: () => setTimeout, }); -@Injectable() -@dataService(PROCESS) +@Injectable({ providedIn: 'root' }) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData, SearchData { - private findAllData: FindAllData; private deleteData: DeleteData; private searchData: SearchData; @@ -51,7 +70,7 @@ export class ProcessDataService extends IdentifiableDataService impleme protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, protected zone: NgZone, - @Inject(TIMER_FACTORY) protected timer: (callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout + @Inject(TIMER_FACTORY) protected timer: (callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout, ) { super('processes', requestService, rdbService, objectCache, halService); @@ -82,7 +101,7 @@ export class ProcessDataService extends IdentifiableDataService impleme */ getFilesEndpoint(processId: string): Observable { return this.getBrowseEndpoint().pipe( - switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`)) + switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`)), ); } @@ -144,13 +163,13 @@ export class ProcessDataService extends IdentifiableDataService impleme autoRefreshingSearchBy(id: string, searchMethod: string, options?: FindListOptions, pollingIntervalInMs: number = 5000, ...linksToFollow: FollowLinkConfig[]): Observable>> { const result$ = this.searchBy(searchMethod, options, true, true, ...linksToFollow).pipe( - getAllCompletedRemoteData() + getAllCompletedRemoteData(), ); const sub = result$.pipe( filter(() => - !this.activelyBeingPolled.has(id) - ) + !this.activelyBeingPolled.has(id), + ), ).subscribe((processListRd: RemoteData>) => { this.clearCurrentTimeout(id); const nextTimeout = this.timer(() => { @@ -241,8 +260,8 @@ export class ProcessDataService extends IdentifiableDataService impleme const sub = process$.pipe( filter((processRD: RemoteData) => !ProcessDataService.hasCompletedOrFailed(processRD.payload) && - !this.activelyBeingPolled.has(processId) - ) + !this.activelyBeingPolled.has(processId), + ), ).subscribe((processRD: RemoteData) => { this.clearCurrentTimeout(processId); if (processRD.hasSucceeded) { @@ -261,7 +280,7 @@ export class ProcessDataService extends IdentifiableDataService impleme // observable) that unsubscribes the previous one, removes the processId from the list of // processes being polled and clears any running timeouts process$.pipe( - find((processRD: RemoteData) => ProcessDataService.hasCompletedOrFailed(processRD.payload)) + find((processRD: RemoteData) => ProcessDataService.hasCompletedOrFailed(processRD.payload)), ).subscribe(() => { this.stopAutoRefreshing(processId); }); @@ -269,7 +288,7 @@ export class ProcessDataService extends IdentifiableDataService impleme return process$.pipe( distinctUntilChanged((previous: RemoteData, current: RemoteData) => previous.payload?.processStatus === current.payload?.processStatus, - ) + ), ); } } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index d9c92cb1d21..e61da4db636 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -1,34 +1,38 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + map, + take, +} from 'rxjs/operators'; + +import { Process } from '../../../process-page/processes/process.model'; +import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; +import { Script } from '../../../process-page/scripts/script.model'; +import { hasValue } from '../../../shared/empty.util'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { Script } from '../../../process-page/scripts/script.model'; -import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; -import { map, take } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; +import { + FindAllData, + FindAllDataImpl, +} from '../base/find-all-data'; +import { IdentifiableDataService } from '../base/identifiable-data.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; import { RemoteData } from '../remote-data'; import { MultipartPostRequest } from '../request.models'; import { RequestService } from '../request.service'; -import { Observable } from 'rxjs'; -import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; -import { Process } from '../../../process-page/processes/process.model'; -import { hasValue } from '../../../shared/empty.util'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; import { RestRequest } from '../rest-request.model'; -import { IdentifiableDataService } from '../base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; -import { FindListOptions } from '../find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { PaginatedList } from '../paginated-list.model'; -import { dataService } from '../base/data-service.decorator'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; export const BATCH_IMPORT_SCRIPT_NAME = 'import'; export const BATCH_EXPORT_SCRIPT_NAME = 'export'; -@Injectable() -@dataService(SCRIPT) +@Injectable({ providedIn: 'root' }) export class ScriptDataService extends IdentifiableDataService