diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 219074780e3..e2680420a21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,11 +43,11 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v3 + uses: actions/checkout@v4 # https://github.com/actions/setup-node - name: Install Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -118,7 +118,7 @@ jobs: # https://github.com/cypress-io/github-action # (NOTE: to run these e2e tests locally, just use 'ng e2e') - name: Run e2e tests (integration tests) - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: # Run tests in Chrome, headless mode (default) browser: chrome @@ -191,7 +191,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Download artifacts from previous 'tests' job - name: Download coverage artifacts @@ -203,10 +203,14 @@ jobs: # Retry action: https://github.com/marketplace/actions/retry-action # Codecov action: https://github.com/codecov/codecov-action - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.0.36 + uses: Wandalen/wretry.action@v1.3.0 with: action: codecov/codecov-action@v3 - # Try upload 5 times max + # 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 + # Try re-running action 5 times max attempt_limit: 5 # Run again in 30 seconds attempt_delay: 30000 diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 8b415296c71..d96e786cc37 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -35,7 +35,7 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0c36d5af987..85a72161131 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,6 +3,9 @@ name: Docker images # Run this Build for all pushes to 'main' or maintenance branches, or tagged releases. # Also run for PRs to ensure PR doesn't break Docker build process +# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images +# https://github.com/DSpace/DSpace/blob/main/.github/workflows/reusable-docker-build.yml +# on: push: branches: @@ -15,82 +18,22 @@ on: permissions: contents: read # to fetch code (actions/checkout) - -env: - # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) - # For a new commit on default branch (main), use the literal tag 'latest' on Docker image. - # For a new commit on other branches, use the branch name as the tag for Docker image. - # For a new tag, copy that tag name as the tag for Docker image. - IMAGE_TAGS: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=tag - # Define default tag "flavor" for docker/metadata-action per - # https://github.com/docker/metadata-action#flavor-input - # We manage the 'latest' tag ourselves to the 'main' branch (see settings above) - TAGS_FLAVOR: | - latest=false - # Architectures / Platforms for which we will build Docker images - # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. - # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. - PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }} - - jobs: - ############################################### + ############################################################# # Build/Push the 'dspace/dspace-angular' image - ############################################### + ############################################################# dspace-angular: # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' if: github.repository == 'dspace/dspace-angular' - runs-on: ubuntu-latest - - steps: - # https://github.com/actions/checkout - - name: Checkout codebase - uses: actions/checkout@v3 - - # https://github.com/docker/setup-buildx-action - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v2 - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU emulation to build for multiple architectures - uses: docker/setup-qemu-action@v2 - - # https://github.com/docker/login-action - - name: Login to DockerHub - # Only login if not a PR, as PRs only trigger a Docker build and not a push - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - # https://github.com/docker/metadata-action - # Get Metadata for docker_build step below - - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image - id: meta_build - uses: docker/metadata-action@v4 - with: - images: dspace/dspace-angular - tags: ${{ env.IMAGE_TAGS }} - flavor: ${{ env.TAGS_FLAVOR }} - - # https://github.com/docker/build-push-action - - name: Build and push 'dspace-angular' image - id: docker_build - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile - platforms: ${{ env.PLATFORMS }} - # For pull requests, we run the Docker build (to ensure no PR changes break the build), - # but we ONLY do an image push to DockerHub if it's NOT a PR - push: ${{ github.event_name != 'pull_request' }} - # Use tags / labels provided by 'docker/metadata-action' above - tags: ${{ steps.meta_build.outputs.tags }} - labels: ${{ steps.meta_build.outputs.labels }} + # 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 + image_name: dspace/dspace-angular + dockerfile_path: ./Dockerfile + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} ############################################################# # Build/Push the 'dspace/dspace-angular' image ('-dist' tag) @@ -98,53 +41,19 @@ jobs: dspace-angular-dist: # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' if: github.repository == 'dspace/dspace-angular' - runs-on: ubuntu-latest - - steps: - # https://github.com/actions/checkout - - name: Checkout codebase - uses: actions/checkout@v3 - - # https://github.com/docker/setup-buildx-action - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v2 - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU emulation to build for multiple architectures - uses: docker/setup-qemu-action@v2 - - # https://github.com/docker/login-action - - name: Login to DockerHub - # Only login if not a PR, as PRs only trigger a Docker build and not a push - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - # https://github.com/docker/metadata-action - # Get Metadata for docker_build_dist step below - - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image - id: meta_build_dist - uses: docker/metadata-action@v4 - with: - images: dspace/dspace-angular - tags: ${{ env.IMAGE_TAGS }} - # As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same - # tagging logic as the primary 'dspace/dspace-angular' image above. - flavor: ${{ env.TAGS_FLAVOR }} - suffix=-dist - - - name: Build and push 'dspace-angular-dist' image - id: docker_build_dist - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile.dist - platforms: ${{ env.PLATFORMS }} - # For pull requests, we run the Docker build (to ensure no PR changes break the build), - # but we ONLY do an image push to DockerHub if it's NOT a PR - push: ${{ github.event_name != 'pull_request' }} - # Use tags / labels provided by 'docker/metadata-action' above - tags: ${{ steps.meta_build_dist.outputs.tags }} - labels: ${{ steps.meta_build_dist.outputs.labels }} + # 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-dist + image_name: dspace/dspace-angular + dockerfile_path: ./Dockerfile.dist + # As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same + # tagging logic as the primary 'dspace/dspace-angular' image above. + tags_flavor: suffix=-dist + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} + # Enable redeploy of sandbox & demo if the branch for this image matches the deployment branch of + # these sites as specified in reusable-docker-build.xml + REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }} + REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }} \ No newline at end of file diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml index 109835d14d3..857f22755e4 100644 --- a/.github/workflows/port_merged_pull_request.yml +++ b/.github/workflows/port_merged_pull_request.yml @@ -23,11 +23,11 @@ jobs: if: github.event.pull_request.merged steps: # Checkout code - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Port PR to other branch (ONLY if labeled with "port to") # See https://github.com/korthout/backport-action - name: Create backport pull requests - uses: korthout/backport-action@v1 + uses: korthout/backport-action@v2 with: # Trigger based on a "port to [branch]" label on PR # (This label must specify the branch name to port to) diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index 9b61af72d18..f16e81c9fd2 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@v1.6.2 + uses: toshimaru/auto-author-assign@v2.0.1 diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts index 1a9b841eb7d..9852216e438 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -8,11 +8,6 @@ describe('Header', () => { cy.get('ds-header').should('be.visible'); // Analyze for accessibility - testA11y({ - include: ['ds-header'], - exclude: [ - ['#search-navbar-container'] // search in navbar has duplicative ID. Will be fixed in #1174 - ], - }); + testA11y('ds-header'); }); }); diff --git a/package.json b/package.json index 553962dfeb7..e5347742c8c 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "ngx-infinite-scroll": "^15.0.0", "ngx-pagination": "6.0.3", "ngx-sortablejs": "^11.1.0", - "ngx-ui-switch": "^14.0.3", + "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", "pem": "1.14.7", "prop-types": "^15.8.1", diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html index c716aedb8b3..6e967b53b5e 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html @@ -1,15 +1,15 @@ -
-
-
+
+
@@ -17,51 +17,53 @@
- -
+
diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html index 01f36ef03f4..c41053874e7 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html @@ -1,13 +1,13 @@ -
- -
-
+
+
@@ -15,7 +15,7 @@
- + 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 4979f858193..bf7b9a2060d 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -2,7 +2,7 @@
- +

{{labelPrefix + 'head' | translate}}

- +
diff --git a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html index f57ec9cd382..efcced2a87c 100644 --- a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html +++ b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html @@ -1,11 +1,11 @@
-

{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}

+

{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}

-
\ No newline at end of file +
diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts index d0827e6e4d5..4dec129eadd 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, combineLatest as observableCombineLatest, @@ -32,7 +32,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; * A component used for managing all existing metadata fields within the current metadata schema. * The admin can create, edit or delete metadata fields here. */ -export class MetadataSchemaComponent implements OnInit { +export class MetadataSchemaComponent implements OnInit, OnDestroy { /** * The metadata schema */ @@ -60,7 +60,6 @@ export class MetadataSchemaComponent implements OnInit { constructor(private registryService: RegistryService, private route: ActivatedRoute, private notificationsService: NotificationsService, - private router: Router, private paginationService: PaginationService, private translateService: TranslateService) { @@ -86,7 +85,7 @@ export class MetadataSchemaComponent implements OnInit { */ private updateFields() { this.metadataFields$ = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( - switchMap((currentPagination) => combineLatest(this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination))), + switchMap((currentPagination) => combineLatest([this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination)])), switchMap(([schema, update, currentPagination]: [MetadataSchema, boolean, PaginationComponentOptions]) => { if (update) { this.needsUpdate$.next(false); @@ -193,10 +192,10 @@ export class MetadataSchemaComponent implements OnInit { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( + const messages = observableCombineLatest([ this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }) - ); + ]); messages.subscribe(([head, content]) => { if (success) { this.notificationsService.success(head, content); @@ -207,6 +206,7 @@ export class MetadataSchemaComponent implements OnInit { } ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.registryService.deselectAllMetadataField(); } } diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts index 3168ea93c92..30f801cecb7 100644 --- a/src/app/admin/admin-routing-paths.ts +++ b/src/app/admin/admin-routing-paths.ts @@ -2,7 +2,12 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAdminModuleRoute } from '../app-routing-paths'; export const REGISTRIES_MODULE_PATH = 'registries'; +export const NOTIFICATIONS_MODULE_PATH = 'notifications'; export function getRegistriesModuleRoute() { return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); } + +export function getNotificationsModuleRoute() { + return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); +} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index 8e4f13b1641..a7d19a69357 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -6,12 +6,17 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; -import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; +import { REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; @NgModule({ imports: [ RouterModule.forChild([ + { + path: NOTIFICATIONS_MODULE_PATH, + loadChildren: () => import('./admin-notifications/admin-notifications.module') + .then((m) => m.AdminNotificationsModule), + }, { path: REGISTRIES_MODULE_PATH, loadChildren: () => import('./admin-registries/admin-registries.module') diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html index fe7e5595ab0..ca69501382e 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html @@ -34,22 +34,21 @@

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

diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts index 88efd2a711e..9522be29ce3 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -143,7 +143,7 @@ describe('AdminSidebarComponent', () => { describe('when the collapse link is clicked', () => { beforeEach(() => { spyOn(menuService, 'toggleMenu'); - const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > a')); + const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > button')); sidebarToggler.triggerEventHandler('click', { preventDefault: () => {/**/ } diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html index af3afe98f87..1467ffb0cc8 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html @@ -1,5 +1,5 @@
-

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

+

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

-

{{ 'collection.edit.tabs.source.head' | translate }}

+

{{ 'collection.edit.tabs.source.head' | translate }}

diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html index 20afd701ffc..8d095dd2296 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -2,7 +2,7 @@
-

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

+

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

diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.html b/src/app/community-page/delete-community-page/delete-community-page.component.html index 6bb8460bc95..ef5fdade73b 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.html +++ b/src/app/community-page/delete-community-page/delete-community-page.component.html @@ -2,7 +2,7 @@
- +

{{ 'community.delete.head' | translate}}

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

diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/community-page/edit-community-page/community-curate/community-curate.component.html index 6c041d17253..5e11fdfbcea 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.html +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.html @@ -1,5 +1,5 @@
-

{{'community.curate.header' |translate:{community: (communityName$ |async)} }}

+

{{'community.curate.header' |translate:{community: (communityName$ |async)} }}

diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index ddd97705b01..8e4fb771c64 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -50,7 +50,7 @@ export class DSONameService { } }, OrgUnit: (dso: DSpaceObject): string => { - return dso.firstMetadataValue('organization.legalName'); + return dso.firstMetadataValue('organization.legalName') || this.translateService.instant('dso.name.untitled'); }, Default: (dso: DSpaceObject): string => { // If object doesn't have dc.title metadata use name property @@ -106,7 +106,7 @@ export class DSONameService { } return `${familyName}, ${givenName}`; } else if (entityType === 'OrgUnit') { - return this.firstMetadataValue(object, dso, 'organization.legalName'); + return this.firstMetadataValue(object, dso, 'organization.legalName') || this.translateService.instant('dso.name.untitled'); } return this.firstMetadataValue(object, dso, 'dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); } diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts new file mode 100644 index 00000000000..3544af62e7a --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -0,0 +1,31 @@ +import {QualityAssuranceBreadcrumbResolver} from './quality-assurance-breadcrumb.resolver'; + +describe('QualityAssuranceBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: QualityAssuranceBreadcrumbResolver; + let qualityAssuranceBreadcrumbService: any; + let route: any; + const fullPath = '/test/quality-assurance/'; + const expectedKey = 'testSourceId:testTopicId'; + + beforeEach(() => { + route = { + paramMap: { + get: function (param) { + return this[param]; + }, + sourceId: 'testSourceId', + topicId: 'testTopicId' + } + }; + qualityAssuranceBreadcrumbService = {}; + resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); + }); + + it('should resolve the breadcrumb config', () => { + const resolvedConfig = resolver.resolve(route as any, {url: fullPath + 'testSourceId'} as any); + 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 new file mode 100644 index 00000000000..6eb351ab1ab --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -0,0 +1,32 @@ +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'; + +@Injectable({ + providedIn: 'root' +}) +export class QualityAssuranceBreadcrumbResolver implements Resolve> { + constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} + + /** + * 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; + + if (topicId) { + key += `:${topicId}`; + } + const fullPath = state.url; + const url = fullPath.substr(0, fullPath.indexOf(sourceId)); + + return { provider: this.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 new file mode 100644 index 00000000000..4fef7672147 --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts @@ -0,0 +1,40 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { getTestScheduler } from 'jasmine-marbles'; +import {QualityAssuranceBreadcrumbService} from './quality-assurance-breadcrumb.service'; + +describe('QualityAssuranceBreadcrumbService', () => { + let service: QualityAssuranceBreadcrumbService; + let dataService: any; + let translateService: any = { + instant: (str) => str, + }; + + let exampleString; + let exampleURL; + let exampleQaKey; + + function init() { + exampleString = 'sourceId'; + exampleURL = '/test/quality-assurance/'; + exampleQaKey = 'admin.quality-assurance.breadcrumbs'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new QualityAssuranceBreadcrumbService(dataService,translateService); + }); + + describe('getBreadcrumbs', () => { + 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)] + }); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts new file mode 100644 index 00000000000..209ae0722ce --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts @@ -0,0 +1,53 @@ +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; +import { Observable, of as observableOf } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { QualityAssuranceTopicDataService } from '../notifications/qa/topics/quality-assurance-topic-data.service'; + + + +/** + * Service to calculate QA breadcrumbs for a single part of the route + */ +@Injectable({ + providedIn: 'root' +}) +export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderService { + + private QUALITY_ASSURANCE_BREADCRUMB_KEY = 'admin.quality-assurance.breadcrumbs'; + constructor( + protected qualityAssuranceService: QualityAssuranceTopicDataService, + private translationService: TranslateService, + ) { + + } + + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ + getBreadcrumbs(key: string, url: string): Observable { + const sourceId = key.split(':')[0]; + const topicId = key.split(':')[1]; + + if (topicId) { + return this.qualityAssuranceService.getTopic(topicId).pipe( + getFirstCompletedRemoteData(), + map((topic) => { + return [new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), + new Breadcrumb(sourceId, `${url}${sourceId}`), + new Breadcrumb(topicId, undefined)]; + }) + ); + } else { + return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), + new Breadcrumb(sourceId, `${url}${sourceId}`)]); + } + + } +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index dbca773375a..b3abf5f877e 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -157,6 +157,9 @@ 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'; @@ -369,9 +372,12 @@ export const models = ShortLivedToken, Registration, UsageReport, + QualityAssuranceTopicObject, + QualityAssuranceEventObject, Root, SearchConfig, SubmissionAccessesModel, + QualityAssuranceSourceObject, AccessStatusObject, ResearcherProfile, OrcidQueue, diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 8fef45a9532..e2943f1762f 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -34,4 +34,5 @@ export enum FeatureID { CanEditItem = 'canEditItem', CanRegisterDOI = 'canRegisterDOI', CanSubscribe = 'canSubscribeDso', + CanSeeQA = 'canSeeQA' } diff --git a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts new file mode 100644 index 00000000000..50d0e43a99c --- /dev/null +++ b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts @@ -0,0 +1,247 @@ +import { HttpClient } from '@angular/common/http'; + +import { TestScheduler } from 'rxjs/testing'; +import { of as observableOf } from 'rxjs'; +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { RequestService } from '../../../data/request.service'; +import { buildPaginatedList } from '../../../data/paginated-list.model'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { RestResponse } from '../../../cache/response.models'; +import { PageInfo } from '../../../shared/page-info.model'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject } from '../../../../shared/remote-data.utils'; +import { QualityAssuranceEventDataService } from './quality-assurance-event-data.service'; +import { + qualityAssuranceEventObjectMissingPid, + qualityAssuranceEventObjectMissingPid2, + qualityAssuranceEventObjectMissingProjectFound +} from '../../../../shared/mocks/notifications.mock'; +import { ReplaceOperation } from 'fast-json-patch'; +import { RequestEntry } from '../../../data/request-entry.model'; +import { FindListOptions } from '../../../data/find-list-options.model'; + +describe('QualityAssuranceEventDataService', () => { + let scheduler: TestScheduler; + let service: QualityAssuranceEventDataService; + let serviceASAny: any; + let responseCacheEntry: RequestEntry; + let responseCacheEntryB: RequestEntry; + let responseCacheEntryC: RequestEntry; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparator: any; + + const endpointURL = 'https://rest.api/rest/api/integration/qualityassurancetopics'; + const requestUUID = '8b3c913a-5a4b-438b-9181-be1a5b4a1c8a'; + const topic = 'ENRICH!MORE!PID'; + + const pageInfo = new PageInfo(); + const array = [qualityAssuranceEventObjectMissingPid, qualityAssuranceEventObjectMissingPid2]; + const paginatedList = buildPaginatedList(pageInfo, array); + const qaEventObjectRD = createSuccessfulRemoteDataObject(qualityAssuranceEventObjectMissingPid); + const qaEventObjectMissingProjectRD = createSuccessfulRemoteDataObject(qualityAssuranceEventObjectMissingProjectFound); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + const status = 'ACCEPTED'; + const operation: ReplaceOperation[] = [ + { + path: '/status', + op: 'replace', + value: status + } + ]; + + beforeEach(() => { + scheduler = getTestScheduler(); + + 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: jasmine.createSpy('getByHref'), + getByUUID: jasmine.createSpy('getByUUID') + }); + + responseCacheEntryB = new RequestEntry(); + responseCacheEntryB.request = { href: 'https://rest.api/' } as any; + responseCacheEntryB.response = new RestResponse(true, 201, 'Created'); + + responseCacheEntryC = new RequestEntry(); + responseCacheEntryC.request = { href: 'https://rest.api/' } as any; + responseCacheEntryC.response = new RestResponse(true, 204, 'No Content'); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('(a)', { + a: qaEventObjectRD + }), + buildList: cold('(a)', { + a: paginatedListRD + }), + buildFromRequestUUID: jasmine.createSpy('buildFromRequestUUID'), + buildFromRequestUUIDAndAwait: jasmine.createSpy('buildFromRequestUUIDAndAwait') + }); + + objectCache = {} as ObjectCacheService; + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a|', { a: endpointURL }) + }); + + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = {} as any; + + service = new QualityAssuranceEventDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + comparator + ); + + serviceASAny = service; + + spyOn(serviceASAny.searchData, 'searchBy').and.callThrough(); + spyOn(serviceASAny, 'findById').and.callThrough(); + spyOn(serviceASAny.patchData, 'patch').and.callThrough(); + spyOn(serviceASAny, 'postOnRelated').and.callThrough(); + spyOn(serviceASAny, 'deleteOnRelated').and.callThrough(); + }); + + describe('getEventsByTopic', () => { + beforeEach(() => { + serviceASAny.requestService.getByHref.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.requestService.getByUUID.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.rdbService.buildFromRequestUUID.and.returnValue(observableOf(qaEventObjectRD)); + }); + + it('should proxy the call to searchData.searchBy', () => { + const options: FindListOptions = { + searchParams: [ + { + fieldName: 'topic', + fieldValue: topic + } + ] + }; + service.getEventsByTopic(topic); + expect(serviceASAny.searchData.searchBy).toHaveBeenCalledWith('findByTopic', options, true, true); + }); + + it('should return a RemoteData> for the object with the given Topic', () => { + const result = service.getEventsByTopic(topic); + const expected = cold('(a)', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('getEvent', () => { + beforeEach(() => { + serviceASAny.requestService.getByHref.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.requestService.getByUUID.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.rdbService.buildFromRequestUUID.and.returnValue(observableOf(qaEventObjectRD)); + }); + + it('should call findById', () => { + service.getEvent(qualityAssuranceEventObjectMissingPid.id).subscribe( + (res) => { + expect(serviceASAny.findById).toHaveBeenCalledWith(qualityAssuranceEventObjectMissingPid.id, true, true); + } + ); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.getEvent(qualityAssuranceEventObjectMissingPid.id); + const expected = cold('(a)', { + a: qaEventObjectRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('patchEvent', () => { + beforeEach(() => { + serviceASAny.requestService.getByHref.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.requestService.getByUUID.and.returnValue(observableOf(responseCacheEntry)); + serviceASAny.rdbService.buildFromRequestUUID.and.returnValue(observableOf(qaEventObjectRD)); + serviceASAny.rdbService.buildFromRequestUUIDAndAwait.and.returnValue(observableOf(qaEventObjectRD)); + }); + + it('should proxy the call to patchData.patch', () => { + service.patchEvent(status, qualityAssuranceEventObjectMissingPid).subscribe( + (res) => { + expect(serviceASAny.patchData.patch).toHaveBeenCalledWith(qualityAssuranceEventObjectMissingPid, operation); + } + ); + }); + + it('should return a RemoteData with HTTP 200', () => { + const result = service.patchEvent(status, qualityAssuranceEventObjectMissingPid); + const expected = cold('(a|)', { + a: createSuccessfulRemoteDataObject(qualityAssuranceEventObjectMissingPid) + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('boundProject', () => { + beforeEach(() => { + serviceASAny.requestService.getByHref.and.returnValue(observableOf(responseCacheEntryB)); + serviceASAny.requestService.getByUUID.and.returnValue(observableOf(responseCacheEntryB)); + serviceASAny.rdbService.buildFromRequestUUID.and.returnValue(observableOf(qaEventObjectMissingProjectRD)); + }); + + it('should call postOnRelated', () => { + service.boundProject(qualityAssuranceEventObjectMissingProjectFound.id, requestUUID).subscribe( + (res) => { + expect(serviceASAny.postOnRelated).toHaveBeenCalledWith(qualityAssuranceEventObjectMissingProjectFound.id, requestUUID); + } + ); + }); + + it('should return a RestResponse with HTTP 201', () => { + const result = service.boundProject(qualityAssuranceEventObjectMissingProjectFound.id, requestUUID); + const expected = cold('(a|)', { + a: createSuccessfulRemoteDataObject(qualityAssuranceEventObjectMissingProjectFound) + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('removeProject', () => { + beforeEach(() => { + serviceASAny.requestService.getByHref.and.returnValue(observableOf(responseCacheEntryC)); + serviceASAny.requestService.getByUUID.and.returnValue(observableOf(responseCacheEntryC)); + serviceASAny.rdbService.buildFromRequestUUID.and.returnValue(observableOf(createSuccessfulRemoteDataObject({}))); + }); + + it('should call deleteOnRelated', () => { + service.removeProject(qualityAssuranceEventObjectMissingProjectFound.id).subscribe( + (res) => { + expect(serviceASAny.deleteOnRelated).toHaveBeenCalledWith(qualityAssuranceEventObjectMissingProjectFound.id); + } + ); + }); + + it('should return a RestResponse with HTTP 204', () => { + const result = service.removeProject(qualityAssuranceEventObjectMissingProjectFound.id); + const expected = cold('(a|)', { + a: createSuccessfulRemoteDataObject({}) + }); + expect(result).toBeObservable(expected); + }); + }); + +}); diff --git a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts new file mode 100644 index 00000000000..7f7e68afaab --- /dev/null +++ b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts @@ -0,0 +1,203 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { find, take } from 'rxjs/operators'; +import { ReplaceOperation } from 'fast-json-patch'; + +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { dataService } from '../../../data/base/data-service.decorator'; +import { RequestService } from '../../../data/request.service'; +import { RemoteData } from '../../../data/remote-data'; +import { QualityAssuranceEventObject } from '../models/quality-assurance-event.model'; +import { QUALITY_ASSURANCE_EVENT_OBJECT } from '../models/quality-assurance-event-object.resource-type'; +import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../../data/paginated-list.model'; +import { NoContent } from '../../../shared/NoContent.model'; +import { FindListOptions } from '../../../data/find-list-options.model'; +import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; +import { CreateData, CreateDataImpl } from '../../../data/base/create-data'; +import { PatchData, PatchDataImpl } from '../../../data/base/patch-data'; +import { DeleteData, DeleteDataImpl } from '../../../data/base/delete-data'; +import { SearchData, SearchDataImpl } from '../../../data/base/search-data'; +import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; +import { hasValue } from '../../../../shared/empty.util'; +import { DeleteByIDRequest, PostRequest } from '../../../data/request.models'; + +/** + * The service handling all Quality Assurance topic REST requests. + */ +@Injectable() +@dataService(QUALITY_ASSURANCE_EVENT_OBJECT) +export class QualityAssuranceEventDataService extends IdentifiableDataService { + + private createData: CreateData; + private searchData: SearchData; + private patchData: PatchData; + private deleteData: DeleteData; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {DefaultChangeAnalyzer} comparator + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected comparator: DefaultChangeAnalyzer + ) { + super('qualityassuranceevents', requestService, rdbService, objectCache, halService); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator, this.responseMsToLive, this.constructIdEndpoint); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Return the list of Quality Assurance events by topic. + * + * @param topic + * The Quality Assurance topic + * @param options + * Find list options object. + * @param linksToFollow + * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * @return Observable>> + * The list of Quality Assurance events. + */ + public getEventsByTopic(topic: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { + options.searchParams = [ + { + fieldName: 'topic', + fieldValue: topic + } + ]; + return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow); + } + + /** + * Clear findByTopic requests from cache + */ + public clearFindByTopicRequests() { + this.requestService.setStaleByHrefSubstring('findByTopic'); + } + + /** + * Return a single Quality Assurance event. + * + * @param id + * The Quality Assurance event id + * @param linksToFollow + * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return Observable> + * The Quality Assurance event. + */ + public getEvent(id: string, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.findById(id, true, true, ...linksToFollow); + } + + /** + * Save the new status of a Quality Assurance event. + * + * @param status + * The new status + * @param dso QualityAssuranceEventObject + * The event item + * @param reason + * The optional reason (not used for now; for future implementation) + * @return Observable + * The REST response. + */ + public patchEvent(status, dso, reason?: string): Observable> { + const operation: ReplaceOperation[] = [ + { + path: '/status', + op: 'replace', + value: status + } + ]; + return this.patchData.patch(dso, operation); + } + + /** + * Bound a project to a Quality Assurance event publication. + * + * @param itemId + * The Id of the Quality Assurance event + * @param projectId + * The project Id to bound + * @return Observable + * The REST response. + */ + public boundProject(itemId: string, projectId: string): Observable> { + return this.postOnRelated(itemId, projectId); + } + + /** + * Remove a project from a Quality Assurance event publication. + * + * @param itemId + * The Id of the Quality Assurance event + * @return Observable + * The REST response. + */ + public removeProject(itemId: string): Observable> { + return this.deleteOnRelated(itemId); + } + + /** + * Perform a delete operation on an endpoint related item. Ex.: endpoint//related + * @param objectId The item id + * @return the RestResponse as an Observable + */ + private deleteOnRelated(objectId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.getIDHrefObs(objectId); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + ).subscribe((href: string) => { + const request = new DeleteByIDRequest(requestId, href + '/related', objectId); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.send(request); + }); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + /** + * Perform a post on an endpoint related item with ID. Ex.: endpoint//related?item= + * @param objectId The item id + * @param relatedItemId The related item Id + * @param body The optional POST body + * @return the RestResponse as an Observable + */ + private postOnRelated(objectId: string, relatedItemId: string, body?: any) { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getIDHrefObs(objectId); + + hrefObs.pipe( + take(1) + ).subscribe((href: string) => { + const request = new PostRequest(requestId, href + '/related?item=' + relatedItemId, body); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.send(request); + }); + + return this.rdbService.buildFromRequestUUID(requestId); + } +} diff --git a/src/app/core/notifications/qa/models/quality-assurance-event-object.resource-type.ts b/src/app/core/notifications/qa/models/quality-assurance-event-object.resource-type.ts new file mode 100644 index 00000000000..84aff6ba2cf --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-event-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Quality Assurance event + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const QUALITY_ASSURANCE_EVENT_OBJECT = new ResourceType('qualityassuranceevent'); diff --git a/src/app/core/notifications/qa/models/quality-assurance-event.model.ts b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts new file mode 100644 index 00000000000..0cdb4a57456 --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts @@ -0,0 +1,171 @@ +/* eslint-disable max-classes-per-file */ +import { Observable } from 'rxjs'; +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { QUALITY_ASSURANCE_EVENT_OBJECT } from './quality-assurance-event-object.resource-type'; +import { excludeFromEquals } from '../../../utilities/equals.decorators'; +import { ResourceType } from '../../../shared/resource-type'; +import { HALLink } from '../../../shared/hal-link.model'; +import { Item } from '../../../shared/item.model'; +import { ITEM } from '../../../shared/item.resource-type'; +import { link, typedObject } from '../../../cache/builders/build-decorators'; +import { RemoteData } from '../../../data/remote-data'; +import {CacheableObject} from '../../../cache/cacheable-object.model'; + +/** + * The interface representing the Quality Assurance event message + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface QualityAssuranceEventMessageObject { + +} + +/** + * The interface representing the Quality Assurance event message + */ +export interface SourceQualityAssuranceEventMessageObject { + /** + * The type of 'value' + */ + type: string; + + /** + * The value suggested by Notifications + */ + value: string; + + /** + * The abstract suggested by Notifications + */ + abstract: string; + + /** + * The project acronym suggested by Notifications + */ + acronym: string; + + /** + * The project code suggested by Notifications + */ + code: string; + + /** + * The project funder suggested by Notifications + */ + funder: string; + + /** + * The project program suggested by Notifications + */ + fundingProgram?: string; + + /** + * The project jurisdiction suggested by Notifications + */ + jurisdiction: string; + + /** + * The project title suggested by Notifications + */ + title: string; + + /** + * The Source ID. + */ + sourceId: string; + + /** + * The PID href. + */ + pidHref: string; + +} + +/** + * The interface representing the Quality Assurance event model + */ +@typedObject +export class QualityAssuranceEventObject implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = QUALITY_ASSURANCE_EVENT_OBJECT; + + /** + * The Quality Assurance event uuid inside DSpace + */ + @autoserialize + id: string; + + /** + * The universally unique identifier of this Quality Assurance event + */ + @autoserializeAs(String, 'id') + uuid: string; + + /** + * The Quality Assurance event original id (ex.: the source archive OAI-PMH identifier) + */ + @autoserialize + originalId: string; + + /** + * The title of the article to which the suggestion refers + */ + @autoserialize + title: string; + + /** + * Reliability of the suggestion (of the data inside 'message') + */ + @autoserialize + trust: number; + + /** + * The timestamp Quality Assurance event was saved in DSpace + */ + @autoserialize + eventDate: string; + + /** + * The Quality Assurance event status (ACCEPTED, REJECTED, DISCARDED, PENDING) + */ + @autoserialize + status: string; + + /** + * The suggestion data. Data may vary depending on the source + */ + @autoserialize + message: SourceQualityAssuranceEventMessageObject; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + target: HALLink, + related: HALLink + }; + + /** + * The related publication DSpace item + * Will be undefined unless the {@item HALLink} has been resolved. + */ + @link(ITEM) + target?: Observable>; + + /** + * The related project for this Event + * Will be undefined unless the {@related HALLink} has been resolved. + */ + @link(ITEM) + related?: Observable>; +} diff --git a/src/app/core/notifications/qa/models/quality-assurance-source-object.resource-type.ts b/src/app/core/notifications/qa/models/quality-assurance-source-object.resource-type.ts new file mode 100644 index 00000000000..b4f64b24d14 --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-source-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Quality Assurance source + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const QUALITY_ASSURANCE_SOURCE_OBJECT = new ResourceType('qualityassurancesource'); diff --git a/src/app/core/notifications/qa/models/quality-assurance-source.model.ts b/src/app/core/notifications/qa/models/quality-assurance-source.model.ts new file mode 100644 index 00000000000..f59467384ff --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-source.model.ts @@ -0,0 +1,52 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { excludeFromEquals } from '../../../utilities/equals.decorators'; +import { ResourceType } from '../../../shared/resource-type'; +import { HALLink } from '../../../shared/hal-link.model'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { QUALITY_ASSURANCE_SOURCE_OBJECT } from './quality-assurance-source-object.resource-type'; +import {CacheableObject} from '../../../cache/cacheable-object.model'; + +/** + * The interface representing the Quality Assurance source model + */ +@typedObject +export class QualityAssuranceSourceObject implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = QUALITY_ASSURANCE_SOURCE_OBJECT; + + /** + * The Quality Assurance source id + */ + @autoserialize + id: string; + + /** + * The date of the last udate from Notifications + */ + @autoserialize + lastEvent: string; + + /** + * The total number of suggestions provided by Notifications for this source + */ + @autoserialize + totalEvents: number; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + }; +} diff --git a/src/app/core/notifications/qa/models/quality-assurance-topic-object.resource-type.ts b/src/app/core/notifications/qa/models/quality-assurance-topic-object.resource-type.ts new file mode 100644 index 00000000000..e9fc57a307c --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-topic-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Quality Assurance topic + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const QUALITY_ASSURANCE_TOPIC_OBJECT = new ResourceType('qualityassurancetopic'); diff --git a/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts b/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts new file mode 100644 index 00000000000..529980e5f7c --- /dev/null +++ b/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts @@ -0,0 +1,58 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { QUALITY_ASSURANCE_TOPIC_OBJECT } from './quality-assurance-topic-object.resource-type'; +import { excludeFromEquals } from '../../../utilities/equals.decorators'; +import { ResourceType } from '../../../shared/resource-type'; +import { HALLink } from '../../../shared/hal-link.model'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import {CacheableObject} from '../../../cache/cacheable-object.model'; + +/** + * The interface representing the Quality Assurance topic model + */ +@typedObject +export class QualityAssuranceTopicObject implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = QUALITY_ASSURANCE_TOPIC_OBJECT; + + /** + * The Quality Assurance topic id + */ + @autoserialize + id: string; + + /** + * The Quality Assurance topic name to display + */ + @autoserialize + name: string; + + /** + * The date of the last udate from Notifications + */ + @autoserialize + lastEvent: string; + + /** + * The total number of suggestions provided by Notifications for this topic + */ + @autoserialize + totalEvents: number; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + }; +} diff --git a/src/app/core/notifications/qa/source/quality-assurance-source-data.service.spec.ts b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.spec.ts new file mode 100644 index 00000000000..50d9251bb88 --- /dev/null +++ b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.spec.ts @@ -0,0 +1,125 @@ +import { HttpClient } from '@angular/common/http'; + +import { TestScheduler } from 'rxjs/testing'; +import { of as observableOf } from 'rxjs'; +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { RequestService } from '../../../data/request.service'; +import { buildPaginatedList } from '../../../data/paginated-list.model'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { RestResponse } from '../../../cache/response.models'; +import { PageInfo } from '../../../shared/page-info.model'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject } from '../../../../shared/remote-data.utils'; +import { + qualityAssuranceSourceObjectMoreAbstract, + qualityAssuranceSourceObjectMorePid +} from '../../../../shared/mocks/notifications.mock'; +import { RequestEntry } from '../../../data/request-entry.model'; +import { QualityAssuranceSourceDataService } from './quality-assurance-source-data.service'; + +describe('QualityAssuranceSourceDataService', () => { + let scheduler: TestScheduler; + let service: QualityAssuranceSourceDataService; + let responseCacheEntry: RequestEntry; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparator: any; + + const endpointURL = 'https://rest.api/rest/api/integration/qualityassurancesources'; + const requestUUID = '8b3c913a-5a4b-438b-9181-be1a5b4a1c8a'; + + const pageInfo = new PageInfo(); + const array = [qualityAssuranceSourceObjectMorePid, qualityAssuranceSourceObjectMoreAbstract]; + const paginatedList = buildPaginatedList(pageInfo, array); + const qaSourceObjectRD = createSuccessfulRemoteDataObject(qualityAssuranceSourceObjectMorePid); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(() => { + scheduler = getTestScheduler(); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('(a)', { + a: qaSourceObjectRD + }), + buildList: cold('(a)', { + a: paginatedListRD + }), + }); + + objectCache = {} as ObjectCacheService; + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a|', { a: endpointURL }) + }); + + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = {} as any; + + service = new QualityAssuranceSourceDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService + ); + + spyOn((service as any).findAllData, 'findAll').and.callThrough(); + spyOn((service as any), 'findById').and.callThrough(); + }); + + describe('getSources', () => { + it('should call findAll', (done) => { + service.getSources().subscribe( + (res) => { + expect((service as any).findAllData.findAll).toHaveBeenCalledWith({}, true, true); + } + ); + done(); + }); + + it('should return a RemoteData> for the object with the given URL', () => { + const result = service.getSources(); + const expected = cold('(a)', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('getSource', () => { + it('should call findById', (done) => { + service.getSource(qualityAssuranceSourceObjectMorePid.id).subscribe( + (res) => { + expect((service as any).findById).toHaveBeenCalledWith(qualityAssuranceSourceObjectMorePid.id, true, true); + } + ); + done(); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.getSource(qualityAssuranceSourceObjectMorePid.id); + const expected = cold('(a)', { + a: qaSourceObjectRD + }); + expect(result).toBeObservable(expected); + }); + }); + +}); diff --git a/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts new file mode 100644 index 00000000000..03a5da2e8c4 --- /dev/null +++ b/src/app/core/notifications/qa/source/quality-assurance-source-data.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; + +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { dataService } from '../../../data/base/data-service.decorator'; +import { RequestService } from '../../../data/request.service'; +import { RemoteData } from '../../../data/remote-data'; +import { QualityAssuranceSourceObject } from '../models/quality-assurance-source.model'; +import { QUALITY_ASSURANCE_SOURCE_OBJECT } from '../models/quality-assurance-source-object.resource-type'; +import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../../data/paginated-list.model'; +import { FindListOptions } from '../../../data/find-list-options.model'; +import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; +import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; + +/** + * The service handling all Quality Assurance source REST requests. + */ +@Injectable() +@dataService(QUALITY_ASSURANCE_SOURCE_OBJECT) +export class QualityAssuranceSourceDataService extends IdentifiableDataService { + + private findAllData: FindAllData; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService + ) { + super('qualityassurancesources', requestService, rdbService, objectCache, halService); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Return the list of Quality Assurance source. + * + * @param options Find list options object. + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * + * @return Observable>> + * The list of Quality Assurance source. + */ + public getSources(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Clear FindAll source requests from cache + */ + public clearFindAllSourceRequests() { + this.requestService.setStaleByHrefSubstring('qualityassurancesources'); + } + + /** + * Return a single Quality Assurance source. + * + * @param id The Quality Assurance source id + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * + * @return Observable> The Quality Assurance source. + */ + public getSource(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } +} diff --git a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts new file mode 100644 index 00000000000..638ee3fa62e --- /dev/null +++ b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.spec.ts @@ -0,0 +1,125 @@ +import { HttpClient } from '@angular/common/http'; + +import { TestScheduler } from 'rxjs/testing'; +import { of as observableOf } from 'rxjs'; +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { RequestService } from '../../../data/request.service'; +import { buildPaginatedList } from '../../../data/paginated-list.model'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { RestResponse } from '../../../cache/response.models'; +import { PageInfo } from '../../../shared/page-info.model'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject } from '../../../../shared/remote-data.utils'; +import { QualityAssuranceTopicDataService } from './quality-assurance-topic-data.service'; +import { + qualityAssuranceTopicObjectMoreAbstract, + qualityAssuranceTopicObjectMorePid +} from '../../../../shared/mocks/notifications.mock'; +import { RequestEntry } from '../../../data/request-entry.model'; + +describe('QualityAssuranceTopicDataService', () => { + let scheduler: TestScheduler; + let service: QualityAssuranceTopicDataService; + let responseCacheEntry: RequestEntry; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparator: any; + + const endpointURL = 'https://rest.api/rest/api/integration/qualityassurancetopics'; + const requestUUID = '8b3c913a-5a4b-438b-9181-be1a5b4a1c8a'; + + const pageInfo = new PageInfo(); + const array = [qualityAssuranceTopicObjectMorePid, qualityAssuranceTopicObjectMoreAbstract]; + const paginatedList = buildPaginatedList(pageInfo, array); + const qaTopicObjectRD = createSuccessfulRemoteDataObject(qualityAssuranceTopicObjectMorePid); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(() => { + scheduler = getTestScheduler(); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('(a)', { + a: qaTopicObjectRD + }), + buildList: cold('(a)', { + a: paginatedListRD + }), + }); + + objectCache = {} as ObjectCacheService; + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a|', { a: endpointURL }) + }); + + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = {} as any; + + service = new QualityAssuranceTopicDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService + ); + + spyOn((service as any).findAllData, 'findAll').and.callThrough(); + spyOn((service as any), 'findById').and.callThrough(); + }); + + describe('getTopics', () => { + it('should call findListByHref', (done) => { + service.getTopics().subscribe( + (res) => { + expect((service as any).findAllData.findAll).toHaveBeenCalledWith({}, true, true); + } + ); + done(); + }); + + it('should return a RemoteData> for the object with the given URL', () => { + const result = service.getTopics(); + const expected = cold('(a)', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('getTopic', () => { + it('should call findByHref', (done) => { + service.getTopic(qualityAssuranceTopicObjectMorePid.id).subscribe( + (res) => { + expect((service as any).findById).toHaveBeenCalledWith(qualityAssuranceTopicObjectMorePid.id, true, true); + } + ); + done(); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.getTopic(qualityAssuranceTopicObjectMorePid.id); + const expected = cold('(a)', { + a: qaTopicObjectRD + }); + expect(result).toBeObservable(expected); + }); + }); + +}); diff --git a/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts new file mode 100644 index 00000000000..2bf5195bf1e --- /dev/null +++ b/src/app/core/notifications/qa/topics/quality-assurance-topic-data.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; + +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../cache/object-cache.service'; +import { RequestService } from '../../../data/request.service'; +import { RemoteData } from '../../../data/remote-data'; +import { QualityAssuranceTopicObject } from '../models/quality-assurance-topic.model'; +import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../../data/paginated-list.model'; +import { FindListOptions } from '../../../data/find-list-options.model'; +import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; +import { dataService } from '../../../data/base/data-service.decorator'; +import { QUALITY_ASSURANCE_TOPIC_OBJECT } from '../models/quality-assurance-topic-object.resource-type'; +import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; + +/** + * The service handling all Quality Assurance topic REST requests. + */ +@Injectable() +@dataService(QUALITY_ASSURANCE_TOPIC_OBJECT) +export class QualityAssuranceTopicDataService extends IdentifiableDataService { + + private findAllData: FindAllData; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService + ) { + super('qualityassurancetopics', requestService, rdbService, objectCache, halService); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Return the list of Quality Assurance topics. + * + * @param options Find list options object. + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * + * @return Observable>> + * The list of Quality Assurance topics. + */ + public getTopics(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Clear FindAll topics requests from cache + */ + public clearFindAllTopicsRequests() { + this.requestService.setStaleByHrefSubstring('qualityassurancetopics'); + } + + /** + * Return a single Quality Assurance topic. + * + * @param id The Quality Assurance topic id + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * + * @return Observable> + * The Quality Assurance topic. + */ + public getTopic(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index 525b42610b4..2701ee24ab3 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -4,6 +4,7 @@
{{ mdValue.newValue.value }}
{{ mdRepresentationName$ | async }} @@ -13,6 +14,7 @@
{{ mdValue.newValue.language }}
diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 13d84e6e2e1..ac903929f6f 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -62,21 +62,22 @@
Footer Content
{{ 'footer.link.lyrasis' | translate}}

-