diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2680420a21..52f20470a3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,8 @@ 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 + COMPOSE_PROJECT_NAME: 'ci' strategy: # Create a matrix of Node versions to test against (in parallel) matrix: diff --git a/config/config.example.yml b/config/config.example.yml index 69a9ffd320f..4b9c1a27ace 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -75,7 +75,7 @@ cache: anonymousCache: # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. # As all pages are cached in server memory, increasing this value will increase memory needs. - # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. max: 0 # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached # copy is automatically refreshed on the next request. @@ -382,7 +382,13 @@ vocabularies: vocabulary: 'srsc' enabled: true -# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' sortDirection: 'ASC' + +# Example of fallback collection for suggestions import +# suggestion: + # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af + # source: "openaire" + diff --git a/cypress/e2e/browse-by-author.cy.ts b/cypress/e2e/browse-by-author.cy.ts index 07c20ad7c91..32c470231d7 100644 --- a/cypress/e2e/browse-by-author.cy.ts +++ b/cypress/e2e/browse-by-author.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Author', () => { cy.visit('/browse/author'); // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); + cy.get('ds-browse-by-metadata').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); + testA11y('ds-browse-by-metadata'); }); }); diff --git a/cypress/e2e/browse-by-dateissued.cy.ts b/cypress/e2e/browse-by-dateissued.cy.ts index 4d22420227c..7966f1c82eb 100644 --- a/cypress/e2e/browse-by-dateissued.cy.ts +++ b/cypress/e2e/browse-by-dateissued.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Date Issued', () => { cy.visit('/browse/dateissued'); // Wait for to be visible - cy.get('ds-browse-by-date-page').should('be.visible'); + cy.get('ds-browse-by-date').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-date-page'); + testA11y('ds-browse-by-date'); }); }); diff --git a/cypress/e2e/browse-by-subject.cy.ts b/cypress/e2e/browse-by-subject.cy.ts index 89b791f03c4..57ca88d1032 100644 --- a/cypress/e2e/browse-by-subject.cy.ts +++ b/cypress/e2e/browse-by-subject.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Subject', () => { cy.visit('/browse/subject'); // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); + cy.get('ds-browse-by-metadata').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); + testA11y('ds-browse-by-metadata'); }); }); diff --git a/cypress/e2e/browse-by-title.cy.ts b/cypress/e2e/browse-by-title.cy.ts index e4e027586a8..09195c30df2 100644 --- a/cypress/e2e/browse-by-title.cy.ts +++ b/cypress/e2e/browse-by-title.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Title', () => { cy.visit('/browse/title'); // Wait for to be visible - cy.get('ds-browse-by-title-page').should('be.visible'); + cy.get('ds-browse-by-title').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-title-page'); + testA11y('ds-browse-by-title'); }); }); diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 40e4974c7c7..31bc53f64d8 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -14,13 +14,8 @@ # Therefore, it should be kept in sync with that file version: "3.7" -networks: - dspacenet: - services: dspace-cli: - networks: - dspacenet: {} environment: # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz diff --git a/docker/cli.yml b/docker/cli.yml index 223ec356b9c..cc266b186f9 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -13,7 +13,13 @@ # # Therefore, it should be kept in sync with that file version: "3.7" - +networks: + # Default to using network named 'dspacenet' from docker-compose-rest.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + # If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in) + default: + name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet + external: true services: dspace-cli: image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" @@ -30,16 +36,12 @@ services: # solr.server: Ensure we are using the 'dspacesolr' image for Solr solr__P__server: http://dspacesolr:8983/solr volumes: - - "assetstore:/dspace/assetstore" + # Keep DSpace assetstore directory between reboots + - assetstore:/dspace/assetstore entrypoint: /dspace/bin/dspace command: help - networks: - - dspacenet tty: true stdin_open: true volumes: assetstore: - -networks: - dspacenet: diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index edbb5b07598..07993e20c62 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -33,11 +33,11 @@ services: # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. solr__D__statistics__P__autoCommit: 'false' + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb - image: dspace/dspace:latest-test networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 @@ -45,8 +45,6 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any) @@ -70,21 +68,18 @@ services: PGDATA: /pgdata image: dspace/dspace-postgres-pgcrypto:loadsql networks: - dspacenet: + - dspacenet stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata # DSpace Solr container dspacesolr: container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim - # Needs main 'dspace' container to start first to guarantee access to solr_configs - depends_on: - - dspace + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet: + - dspacenet ports: - published: 8983 target: 8983 @@ -92,9 +87,6 @@ services: tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data # Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr @@ -103,14 +95,18 @@ services: - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics + precreate-core qaevent /opt/solr/server/solr/configsets/qaevent + cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent exec solr -f volumes: assetstore: pgdata: solr_data: - # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: \ No newline at end of file diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index ea766600efa..e1577ec8375 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -43,7 +43,7 @@ services: depends_on: - dspacedb networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 @@ -51,8 +51,6 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 2. Then, run database migration to init database tables @@ -69,25 +67,23 @@ services: container_name: dspacedb environment: PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto + image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" networks: - dspacenet: + - dspacenet ports: - published: 5432 target: 5432 stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata # DSpace Solr container dspacesolr: container_name: dspacesolr image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" - # Needs main 'dspace' container to start first to guarantee access to solr_configs - depends_on: - - dspace networks: - dspacenet: + - dspacenet ports: - published: 8983 target: 8983 @@ -115,10 +111,10 @@ services: cp -r /opt/solr/server/solr/configsets/search/* search precreate-core statistics /opt/solr/server/solr/configsets/statistics cp -r /opt/solr/server/solr/configsets/statistics/* statistics + precreate-core qaevent /opt/solr/server/solr/configsets/qaevent + cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent exec solr -f volumes: assetstore: pgdata: solr_data: - # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html index 382caf85f46..c164cc5c319 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.html +++ b/src/app/access-control/bulk-access/bulk-access.component.html @@ -1,4 +1,5 @@
+

{{ 'admin.access-control.bulk-access.title' | translate }}

diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index 10924434369..c51ee7597c5 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -1,5 +1,5 @@
- +

{{'admin.batch-import.page.header' | translate}}

{{'admin.batch-import.page.help' | translate}}

selected collection: {{getDspaceObjectName()}}  @@ -28,7 +28,7 @@

{{'admin.batch-import.page.toggle.help' | translate}} - + { + + /** + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsPublicationClaimPageParams { + return { + pageId: route.queryParams.pageId, + pageSize: parseInt(route.queryParams.pageSize, 10), + currentPage: parseInt(route.queryParams.page, 10) + }; + } +} diff --git a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.html b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.html new file mode 100644 index 00000000000..b04e7132f17 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.html @@ -0,0 +1 @@ + diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.scss similarity index 100% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss rename to src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.scss diff --git a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts new file mode 100644 index 00000000000..c5406023ce9 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('AdminNotificationsPublicationClaimPageComponent', () => { + let component: AdminNotificationsPublicationClaimPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot() + ], + declarations: [ + AdminNotificationsPublicationClaimPageComponent + ], + providers: [ + AdminNotificationsPublicationClaimPageComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.ts b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.ts new file mode 100644 index 00000000000..2256a1bc36a --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-admin-notifications-publication-claim-page', + templateUrl: './admin-notifications-publication-claim-page.component.html', + styleUrls: ['./admin-notifications-publication-claim-page.component.scss'] +}) +export class AdminNotificationsPublicationClaimPageComponent { + +} diff --git a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts index 2820a9a2c7b..f92a96d2422 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts @@ -2,6 +2,7 @@ import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { getNotificationsModuleRoute } from '../admin-routing-paths'; export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance'; +export const PUBLICATION_CLAIMS_PATH = 'publication-claim'; export function getQualityAssuranceRoute(id: string) { return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString(); diff --git a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts index 63d555d7b74..07a98aa080d 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -4,6 +4,9 @@ import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../../core/auth/authenticated.guard'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service'; +import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths'; +import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component'; +import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service'; import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths'; import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component'; import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component'; @@ -20,6 +23,21 @@ import { @NgModule({ imports: [ RouterModule.forChild([ + { + canActivate: [ AuthenticatedGuard ], + path: `${PUBLICATION_CLAIMS_PATH}`, + component: AdminNotificationsPublicationClaimPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: I18nBreadcrumbResolver, + suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver + }, + data: { + title: 'admin.notifications.publicationclaim.page.title', + breadcrumbKey: 'admin.notifications.publicationclaim', + showBreadcrumbsFluid: false + } + }, { canActivate: [ AuthenticatedGuard ], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, @@ -71,7 +89,9 @@ import { providers: [ I18nBreadcrumbResolver, I18nBreadcrumbsService, + AdminNotificationsPublicationClaimPageResolver, SourceDataResolver, + AdminQualityAssuranceSourcePageResolver, AdminQualityAssuranceTopicsPageResolver, AdminQualityAssuranceEventsPageResolver, AdminQualityAssuranceSourcePageResolver, diff --git a/src/app/admin/admin-notifications/admin-notifications.module.ts b/src/app/admin/admin-notifications/admin-notifications.module.ts index 84475a1623e..d9efb4c2886 100644 --- a/src/app/admin/admin-notifications/admin-notifications.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications.module.ts @@ -3,10 +3,11 @@ import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module'; +import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component'; import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component'; import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component'; import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component'; -import {NotificationsModule} from '../../notifications/notifications.module'; +import { NotificationsModule } from '../../notifications/notifications.module'; @NgModule({ imports: [ @@ -17,6 +18,7 @@ import {NotificationsModule} from '../../notifications/notifications.module'; NotificationsModule ], declarations: [ + AdminNotificationsPublicationClaimPageComponent, AdminQualityAssuranceTopicsPageComponent, AdminQualityAssuranceEventsPageComponent, AdminQualityAssuranceSourcePageComponent diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html index 37323d41d0e..894ef3dc7e7 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html @@ -19,7 +19,7 @@

{{'admin.registries.bitstream-formats - + @@ -35,6 +35,7 @@

{{'admin.registries.bitstream-formats [checked]="isSelected(bitstreamFormat) | async" (change)="selectBitStreamFormat(bitstreamFormat, $event)" > + {{'admin.registries.bitstream-formats.select' | translate}}}

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index 35bffad185c..d3be5df08e6 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -2,7 +2,7 @@
{{'admin.registries.bitstream-formats.table.selected' | translate}} {{'admin.registries.bitstream-formats.table.id' | translate}} {{'admin.registries.bitstream-formats.table.name' | translate}} {{'admin.registries.bitstream-formats.table.mimetype' | translate}}{{bitstreamFormat.id}}
- + @@ -34,6 +34,7 @@ diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index a4a4613565e..85d1e90692a 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,11 +1,11 @@
-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

{{'admin.registries.metadata.schemas.table.selected' | translate}} {{'admin.registries.metadata.schemas.table.id' | translate}} {{'admin.registries.metadata.schemas.table.namespace' | translate}} {{'admin.registries.metadata.schemas.table.name' | translate}}{{schema.id}}
- + @@ -33,12 +33,11 @@

{{'admin.registries.schema.fields.head' | translate}}

- diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html index c4b737849b6..639c47f7f8c 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts index dab6694f368..0e41a20d847 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts @@ -1,4 +1,4 @@ -import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { Component, ComponentRef, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { @@ -11,9 +11,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; -import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { hasValue } from '../../../../../shared/empty.util'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch) @Component({ @@ -24,17 +25,18 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service /** * The component for displaying a list element for an item search result on the admin search page */ -export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { - @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; +export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnDestroy, OnInit { + @ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective; @ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef; + protected compRef: ComponentRef; + constructor( public dsoNameService: DSONameService, protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService, private themeService: ThemeService, - private componentFactoryResolver: ComponentFactoryResolver, ) { super(dsoNameService, truncatableService, bitstreamDataService); } @@ -44,23 +46,32 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE */ ngOnInit(): void { super.ngOnInit(); - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); + const component: GenericConstructor = this.getComponent(); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; + const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef; viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ - [this.badges.nativeElement], - [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = this.object; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + injector: undefined, + projectableNodes: [ + [this.badges.nativeElement], + [this.buttons.nativeElement], + ], + }, + ); + this.compRef.setInput('object',this.object); + this.compRef.setInput('index', this.index); + this.compRef.setInput('linkType', this.linkType); + this.compRef.setInput('listID', this.listID); + } + + ngOnDestroy(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = undefined; + } } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html index 87bae0c2613..c5c2a5331ad 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index 8035c53547e..b02fa476ea2 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -18,8 +18,8 @@ import { ItemGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component'; import { - ListableObjectDirective -} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; + DynamicComponentLoaderDirective +} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; @@ -38,7 +38,7 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => { let itemRD$; let linkService; let object; - let themeService; + let themeService: ThemeService; function init() { itemRD$ = createSuccessfulRemoteDataObject$(new Item()); @@ -55,7 +55,11 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective], + declarations: [ + WorkflowItemSearchResultAdminWorkflowGridElementComponent, + ItemGridElementComponent, + DynamicComponentLoaderDirective, + ], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts index fd9d21e227d..401140fd82a 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -1,4 +1,4 @@ -import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; +import { Component, ElementRef, ViewChild, ComponentRef, OnDestroy, OnInit } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { @@ -10,7 +10,7 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; -import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { Observable } from 'rxjs'; import { LinkService } from '../../../../../core/cache/builders/link.service'; @@ -24,6 +24,7 @@ import { take } from 'rxjs/operators'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { hasValue } from '../../../../../shared/empty.util'; @listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -34,11 +35,11 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service /** * The component for displaying a grid element for an workflow item on the admin workflow search page */ -export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent { +export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent implements OnDestroy, OnInit { /** * Directive used to render the dynamic component in */ - @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; + @ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective; /** * The html child that contains the badges html @@ -55,9 +56,10 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S */ public item$: Observable; + protected compRef: ComponentRef; + constructor( public dsoNameService: DSONameService, - private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, private themeService: ThemeService, @@ -75,26 +77,34 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S this.dso = this.linkService.resolveLink(this.dso, followLink('item')); this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); this.item$.pipe(take(1)).subscribe((item: Item) => { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); + const component: GenericConstructor = this.getComponent(item); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; - viewContainerRef.clear(); + const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef; + viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + injector: undefined, + projectableNodes: [ [this.badges.nativeElement], - [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = item; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; - componentRef.changeDetectorRef.detectChanges(); - } - ); + [this.buttons.nativeElement], + ], + }, + ); + this.compRef.setInput('object', item); + this.compRef.setInput('index', this.index); + this.compRef.setInput('linkType', this.linkType); + this.compRef.setInput('listID', this.listID); + this.compRef.changeDetectorRef.detectChanges(); + }); + } + + ngOnDestroy(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = undefined; + } } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html index 767ad79786f..f78a0a3ca43 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts index b9e752c1047..d023e57709f 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -20,8 +20,8 @@ import { ItemGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component'; import { - ListableObjectDirective -} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; + DynamicComponentLoaderDirective +} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; @@ -45,7 +45,7 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => { let itemRD$; let linkService; let object; - let themeService; + let themeService: ThemeService; let supervisionOrderDataService; function init() { @@ -67,7 +67,11 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective], + declarations: [ + WorkspaceItemSearchResultAdminWorkflowGridElementComponent, + ItemGridElementComponent, + DynamicComponentLoaderDirective, + ], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts index d6f39e79feb..0fe36056a9a 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts @@ -1,4 +1,4 @@ -import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core'; +import { Component, ElementRef, ViewChild, OnInit, OnDestroy, ComponentRef } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { map, mergeMap, take, tap } from 'rxjs/operators'; @@ -16,9 +16,7 @@ import { import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; -import { - ListableObjectDirective -} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; @@ -37,6 +35,7 @@ import { SupervisionOrder } from '../../../../../core/supervision-order/models/s import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { hasValue } from '../../../../../shared/empty.util'; @listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -47,7 +46,7 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service /** * The component for displaying a grid element for an workflow item on the admin workflow search page */ -export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent implements OnInit { +export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent implements OnDestroy, OnInit { /** * The item linked to the workspace item @@ -67,7 +66,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends /** * Directive used to render the dynamic component in */ - @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; + @ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective; /** * The html child that contains the badges html @@ -79,9 +78,13 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends */ @ViewChild('buttons', { static: true }) buttons: ElementRef; + /** + * The reference to the dynamic component + */ + protected compRef: ComponentRef; + constructor( public dsoNameService: DSONameService, - private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, private themeService: ThemeService, @@ -100,24 +103,24 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends this.dso = this.linkService.resolveLink(this.dso, followLink('item')); this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); this.item$.pipe(take(1)).subscribe((item: Item) => { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); + const component: GenericConstructor = this.getComponent(item); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; + const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef; viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + projectableNodes: [ [this.badges.nativeElement], [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = item; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; - componentRef.changeDetectorRef.detectChanges(); + ], + }); + this.compRef.setInput('object', item); + this.compRef.setInput('index', this.index); + this.compRef.setInput('linkType', this.linkType); + this.compRef.setInput('listID', this.listID); + this.compRef.changeDetectorRef.detectChanges(); } ); @@ -130,6 +133,13 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends }); } + ngOnDestroy(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = undefined; + } + } + /** * Fetch the component depending on the item's entity type, view mode and context * @returns {GenericConstructor} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index deb68f1ea92..a14000aef4d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { RouterModule, NoPreloading } from '@angular/router'; +import { NoPreloading, RouterModule } from '@angular/router'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; @@ -38,8 +38,10 @@ import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; +import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths'; import { MenuResolver } from './menu.resolver'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; +import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard'; @NgModule({ imports: [ @@ -94,7 +96,10 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone path: FORGOT_PASSWORD_PATH, loadChildren: () => import('./forgot-password/forgot-password.module') .then((m) => m.ForgotPasswordModule), - canActivate: [EndUserAgreementCurrentUserGuard] + canActivate: [ + ForgotPasswordCheckGuard, + EndUserAgreementCurrentUserGuard + ] }, { path: COMMUNITY_MODULE_PATH, @@ -202,6 +207,11 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone .then((m) => m.ProcessPageModule), canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, + { path: SUGGESTION_MODULE_PATH, + loadChildren: () => import('./suggestions-page/suggestions-page.module') + .then((m) => m.SuggestionsPageModule), + canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] + }, { path: INFO_MODULE_PATH, loadChildren: () => import('./info/info.module').then((m) => m.InfoModule) diff --git a/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts deleted file mode 100644 index 8eeae0c5de5..00000000000 --- a/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Component} from '@angular/core'; -import { ThemedComponent } from '../../shared/theme-support/themed.component'; -import { BrowseByDatePageComponent } from './browse-by-date-page.component'; -import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator'; - -/** - * Themed wrapper for BrowseByDatePageComponent - * */ -@Component({ - selector: 'ds-themed-browse-by-metadata-page', - styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', -}) - -@rendersBrowseBy(BrowseByDataType.Date) -export class ThemedBrowseByDatePageComponent - extends ThemedComponent { - protected getComponentName(): string { - return 'BrowseByDatePageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-date-page/browse-by-date-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-date-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts similarity index 88% rename from src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts rename to src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts index e41d3a45b25..c5ef9020f1d 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts @@ -1,4 +1,4 @@ -import { BrowseByDatePageComponent } from './browse-by-date-page.component'; +import { BrowseByDateComponent } from './browse-by-date.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; @@ -15,7 +15,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { Community } from '../../core/shared/community.model'; import { Item } from '../../core/shared/item.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; -import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec'; +import { toRemoteData } from '../browse-by-metadata/browse-by-metadata.component.spec'; import { VarDirective } from '../../shared/utils/var.directive'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginationService } from '../../core/pagination/pagination.service'; @@ -23,10 +23,11 @@ import { PaginationServiceStub } from '../../shared/testing/pagination-service.s import { APP_CONFIG } from '../../../config/app-config.interface'; import { environment } from '../../../environments/environment'; import { SortDirection } from '../../core/cache/models/sort-options.model'; +import { cold } from 'jasmine-marbles'; -describe('BrowseByDatePageComponent', () => { - let comp: BrowseByDatePageComponent; - let fixture: ComponentFixture; +describe('BrowseByDateComponent', () => { + let comp: BrowseByDateComponent; + let fixture: ComponentFixture; let route: ActivatedRoute; let paginationService; @@ -86,7 +87,7 @@ describe('BrowseByDatePageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BrowseByDatePageComponent, EnumKeysPipe, VarDirective], + declarations: [BrowseByDateComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: BrowseService, useValue: mockBrowseService }, @@ -101,7 +102,7 @@ describe('BrowseByDatePageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(BrowseByDatePageComponent); + fixture = TestBed.createComponent(BrowseByDateComponent); const browseService = fixture.debugElement.injector.get(BrowseService); spyOn(browseService, 'getFirstItemFor') // ok to expect the default browse as first param since we just need the mock items obtained via sort direction. @@ -112,9 +113,13 @@ describe('BrowseByDatePageComponent', () => { fixture.detectChanges(); }); - it('should initialize the list of items', () => { + it('should initialize the list of items', (done: DoneFn) => { + expect(comp.loading$).toBeObservable(cold('(a|)', { + a: false, + })); comp.items$.subscribe((result) => { expect(result.payload.page).toEqual([firstItem]); + done(); }); }); diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.ts similarity index 68% rename from src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts rename to src/app/browse-by/browse-by-date/browse-by-date.component.ts index c52731a421f..3485519929b 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.ts @@ -1,10 +1,6 @@ -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; -import { - BrowseByMetadataPageComponent, - browseParamsToOptions, - getBrowseSearchOptions -} from '../browse-by-metadata-page/browse-by-metadata-page.component'; -import { combineLatest as observableCombineLatest } from 'rxjs'; +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { BrowseByMetadataComponent, browseParamsToOptions, getBrowseSearchOptions } from '../browse-by-metadata/browse-by-metadata.component'; +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { BrowseService } from '../../core/browse/browse.service'; @@ -19,32 +15,36 @@ import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; @Component({ - selector: 'ds-browse-by-date-page', - styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'], - templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html' + selector: 'ds-browse-by-date', + styleUrls: ['../browse-by-metadata/browse-by-metadata.component.scss'], + templateUrl: '../browse-by-metadata/browse-by-metadata.component.html', }) /** * Component for browsing items by metadata definition of type 'date' * A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * An example would be 'dateissued' for 'dc.date.issued' */ -export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { +@rendersBrowseBy(BrowseByDataType.Date) +export class BrowseByDateComponent extends BrowseByMetadataComponent implements OnInit { /** * The default metadata keys to use for determining the lower limit of the StartsWith dropdown options */ defaultMetadataKeys = ['dc.date.issued']; - public constructor(protected route: ActivatedRoute, - protected browseService: BrowseService, - protected dsoService: DSpaceObjectDataService, - protected router: Router, - protected paginationService: PaginationService, - protected cdRef: ChangeDetectorRef, - @Inject(APP_CONFIG) public appConfig: AppConfig, - public dsoNameService: DSONameService, + public constructor( + protected route: ActivatedRoute, + protected browseService: BrowseService, + protected dsoService: DSpaceObjectDataService, + protected paginationService: PaginationService, + protected router: Router, + @Inject(APP_CONFIG) public appConfig: AppConfig, + public dsoNameService: DSONameService, + protected cdRef: ChangeDetectorRef, ) { super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService); } @@ -57,19 +57,17 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.route.data, + observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.route.data, this.currentPagination$, this.currentSort$]).pipe( - map(([routeParams, queryParams, data, currentPage, currentSort]) => { - return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; + map(([routeParams, queryParams, scope, data, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams, data), scope, currentPage, currentSort]; }) - ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { + ).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => { const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; this.browseId = params.id || this.defaultBrowseId; this.startsWith = +params.startsWith || params.startsWith; - const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails); + const searchOptions = browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails); this.updatePageWithItems(searchOptions, this.value, undefined); - this.updateParent(params.scope); - this.updateLogo(); this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope); })); } @@ -85,12 +83,21 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { * @param scope The scope under which to fetch the earliest item for */ updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) { - const firstItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC); - const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC); + const firstItemRD$: Observable> = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC); + const lastItemRD$: Observable> = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC); + this.loading$ = observableCombineLatest([ + firstItemRD$, + lastItemRD$, + ]).pipe( + map(([firstItemRD, lastItemRD]: [RemoteData, RemoteData]) => firstItemRD.isLoading || lastItemRD.isLoading) + ); this.subs.push( - observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => { - let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit); - let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear()); + observableCombineLatest([ + firstItemRD$, + lastItemRD$, + ]).subscribe(([firstItemRD, lastItemRD]: [RemoteData, RemoteData]) => { + let lowerLimit: number = this.getLimit(firstItemRD, metadataKeys, this.appConfig.browseBy.defaultLowerLimit); + let upperLimit: number = this.getLimit(lastItemRD, metadataKeys, new Date().getUTCFullYear()); const options: number[] = []; const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5; const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10; diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index aac5ba27233..c3840470c6d 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -1,17 +1,13 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; -import { of as observableOf } from 'rxjs'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type'; import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model'; -import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock'; -import { DSONameService } from '../core/breadcrumbs/dso-name.service'; import { RouterStub } from '../shared/testing/router.stub'; describe('BrowseByGuard', () => { describe('canActivate', () => { let guard: BrowseByGuard; - let dsoService: any; let translateService: any; let browseDefinitionService: any; let router: any; @@ -25,10 +21,6 @@ describe('BrowseByGuard', () => { const browseDefinition = Object.assign(new ValueListBrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] }); beforeEach(() => { - dsoService = { - findById: (dsoId: string) => observableOf({ payload: { name: name }, hasSucceeded: true }) - }; - translateService = { instant: () => field }; @@ -39,7 +31,7 @@ describe('BrowseByGuard', () => { router = new RouterStub() as any; - guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, new DSONameServiceMock() as DSONameService, router); + guard = new BrowseByGuard(translateService, browseDefinitionService, router); }); it('should return true, and sets up the data correctly, with a scope and value', () => { @@ -48,6 +40,7 @@ describe('BrowseByGuard', () => { title: field, browseDefinition, }, + parent: null, params: { id, }, @@ -64,7 +57,7 @@ describe('BrowseByGuard', () => { title, id, browseDefinition, - collection: name, + scope, field, value: '"' + value + '"' }; @@ -97,7 +90,7 @@ describe('BrowseByGuard', () => { title, id, browseDefinition, - collection: name, + scope, field, value: '' }; @@ -108,12 +101,48 @@ describe('BrowseByGuard', () => { ); }); + it('should return true, and sets up the data correctly using the community/collection page id, with a scope and without value', () => { + const scopedNoValueRoute = { + data: { + title: field, + browseDefinition, + }, + parent: { + params: { + id: scope, + }, + }, + params: { + id, + }, + queryParams: { + }, + }; + + guard.canActivate(scopedNoValueRoute as any, undefined).pipe( + first(), + ).subscribe((canActivate) => { + const result = { + title, + id, + browseDefinition, + scope, + field, + value: '', + }; + expect(scopedNoValueRoute.data).toEqual(result); + expect(router.navigate).not.toHaveBeenCalled(); + expect(canActivate).toEqual(true); + }); + }); + it('should return true, and sets up the data correctly, without a scope and with a value', () => { const route = { data: { title: field, browseDefinition, }, + parent: null, params: { id, }, @@ -129,7 +158,7 @@ describe('BrowseByGuard', () => { title, id, browseDefinition, - collection: '', + scope: undefined, field, value: '"' + value + '"' }; @@ -147,6 +176,7 @@ describe('BrowseByGuard', () => { data: { title: field, }, + parent: null, params: { id, }, diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index dd87699a84a..cca9a6a6752 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,15 +1,12 @@ -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, Data } from '@angular/router'; import { Injectable } from '@angular/core'; -import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; import { hasNoValue, hasValue } from '../shared/empty.util'; import { map, switchMap } from 'rxjs/operators'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; -import { DSONameService } from '../core/breadcrumbs/dso-name.service'; -import { DSpaceObject } from '../core/shared/dspace-object.model'; import { RemoteData } from '../core/data/remote-data'; import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; @@ -19,11 +16,10 @@ import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; */ export class BrowseByGuard implements CanActivate { - constructor(protected dsoService: DSpaceObjectDataService, - protected translate: TranslateService, - protected browseDefinitionService: BrowseDefinitionDataService, - protected dsoNameService: DSONameService, - protected router: Router, + constructor( + protected translate: TranslateService, + protected browseDefinitionService: BrowseDefinitionDataService, + protected router: Router, ) { } @@ -39,25 +35,14 @@ export class BrowseByGuard implements CanActivate { } else { browseDefinition$ = observableOf(route.data.browseDefinition); } - const scope = route.queryParams.scope; + const scope = route.queryParams.scope ?? route.parent?.params.id; const value = route.queryParams.value; const metadataTranslated = this.translate.instant(`browse.metadata.${id}`); return browseDefinition$.pipe( switchMap((browseDefinition: BrowseDefinition | undefined) => { if (hasValue(browseDefinition)) { - if (hasValue(scope)) { - const dso$: Observable = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteDataPayload()); - return dso$.pipe( - map((dso: DSpaceObject) => { - const name = this.dsoNameService.getName(dso); - route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route); - return true; - }) - ); - } else { - route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route); - return observableOf(true); - } + route.data = this.createData(title, id, browseDefinition, metadataTranslated, value, route, scope); + return observableOf(true); } else { void this.router.navigate([PAGE_NOT_FOUND_PATH]); return observableOf(false); @@ -66,14 +51,14 @@ export class BrowseByGuard implements CanActivate { ); } - private createData(title, id, browseDefinition, collection, field, value, route) { + private createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data { return Object.assign({}, route.data, { title: title, id: id, browseDefinition: browseDefinition, - collection: collection, field: field, - value: hasValue(value) ? `"${value}"` : '' + value: hasValue(value) ? `"${value}"` : '', + scope: scope, }); } } diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html deleted file mode 100644 index cfc2cbe3056..00000000000 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html +++ /dev/null @@ -1,67 +0,0 @@ -
- - -
- -
- - - - - - - - - - - - - - - -
- -
- - -
- -
- -
- - -
-
- - - - -
-
-
-
-
diff --git a/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts deleted file mode 100644 index b0679258e91..00000000000 --- a/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Component} from '@angular/core'; -import { ThemedComponent } from '../../shared/theme-support/themed.component'; -import { BrowseByMetadataPageComponent } from './browse-by-metadata-page.component'; -import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator'; - -/** - * Themed wrapper for BrowseByMetadataPageComponent - **/ -@Component({ - selector: 'ds-themed-browse-by-metadata-page', - styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', -}) - -@rendersBrowseBy(BrowseByDataType.Metadata) -export class ThemedBrowseByMetadataPageComponent - extends ThemedComponent { - protected getComponentName(): string { - return 'BrowseByMetadataPageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-metadata-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html new file mode 100644 index 00000000000..52ef06206f0 --- /dev/null +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html @@ -0,0 +1,21 @@ +
+ +
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss similarity index 100% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts similarity index 91% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts index a5beeb8a452..9fef2b1a35a 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts @@ -1,8 +1,8 @@ import { - BrowseByMetadataPageComponent, + BrowseByMetadataComponent, browseParamsToOptions, getBrowseSearchOptions -} from './browse-by-metadata-page.component'; +} from './browse-by-metadata.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { BrowseService } from '../../core/browse/browse.service'; import { CommonModule } from '@angular/common'; @@ -30,10 +30,11 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { APP_CONFIG } from '../../../config/app-config.interface'; +import { cold } from 'jasmine-marbles'; -describe('BrowseByMetadataPageComponent', () => { - let comp: BrowseByMetadataPageComponent; - let fixture: ComponentFixture; +describe('BrowseByMetadataComponent', () => { + let comp: BrowseByMetadataComponent; + let fixture: ComponentFixture; let browseService: BrowseService; let route: ActivatedRoute; let paginationService; @@ -103,7 +104,7 @@ describe('BrowseByMetadataPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BrowseByMetadataPageComponent, EnumKeysPipe, VarDirective], + declarations: [BrowseByMetadataComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: BrowseService, useValue: mockBrowseService }, @@ -117,7 +118,7 @@ describe('BrowseByMetadataPageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(BrowseByMetadataPageComponent); + fixture = TestBed.createComponent(BrowseByMetadataComponent); comp = fixture.componentInstance; fixture.detectChanges(); browseService = (comp as any).browseService; @@ -144,20 +145,18 @@ describe('BrowseByMetadataPageComponent', () => { route.params = observableOf(paramsWithValue); comp.ngOnInit(); - comp.updateParent('fake-scope'); - comp.updateLogo(); fixture.detectChanges(); }); - it('should fetch items', () => { + it('should fetch items', (done: DoneFn) => { + expect(comp.loading$).toBeObservable(cold('(a|)', { + a: false, + })); comp.items$.subscribe((result) => { expect(result.payload.page).toEqual(mockItems); + done(); }); }); - - it('should fetch the logo', () => { - expect(comp.logo$).toBeTruthy(); - }); }); describe('when calling browseParamsToOptions', () => { @@ -176,7 +175,7 @@ describe('BrowseByMetadataPageComponent', () => { field: 'fake-field', }; - result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author', comp.fetchThumbnails); + result = browseParamsToOptions(paramsScope, 'fake-scope', paginationOptions, sortOptions, 'author', comp.fetchThumbnails); }); it('should return BrowseEntrySearchOptions with the correct properties', () => { diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts similarity index 76% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts index 75dbfcfc82a..00cfedba88a 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts @@ -1,5 +1,5 @@ -import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; -import { Component, Inject, OnInit, OnDestroy } from '@angular/core'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; +import { Component, Inject, OnInit, OnDestroy, Input, OnChanges } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; @@ -12,23 +12,21 @@ import { Item } from '../../core/shared/item.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { filter, map, mergeMap } from 'rxjs/operators'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Bitstream } from '../../core/shared/bitstream.model'; -import { Collection } from '../../core/shared/collection.model'; -import { Community } from '../../core/shared/community.model'; +import { map } from 'rxjs/operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; +import { Context } from '../../core/shared/context.model'; export const BBM_PAGINATION_ID = 'bbm'; @Component({ - selector: 'ds-browse-by-metadata-page', - styleUrls: ['./browse-by-metadata-page.component.scss'], - templateUrl: './browse-by-metadata-page.component.html' + selector: 'ds-browse-by-metadata', + styleUrls: ['./browse-by-metadata.component.scss'], + templateUrl: './browse-by-metadata.component.html', }) /** * Component for browsing (items) by metadata definition. @@ -36,27 +34,40 @@ export const BBM_PAGINATION_ID = 'bbm'; * or multiple metadata fields. An example would be 'author' for * 'dc.contributor.*' */ -export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { +@rendersBrowseBy(BrowseByDataType.Metadata) +export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy { /** - * The list of browse-entries to display + * The optional context */ - browseEntries$: Observable>>; + @Input() context: Context; /** - * The list of items to display when a value is present + * The {@link BrowseByDataType} of this Component */ - items$: Observable>>; + @Input() browseByType: BrowseByDataType; + + /** + * The ID of the {@link Community} or {@link Collection} of the scope to display + */ + @Input() scope: string; + + /** + * Display the h1 title in the section + */ + @Input() displayTitle = true; + + scope$: BehaviorSubject = new BehaviorSubject(undefined); /** - * The current Community or Collection we're browsing metadata/items in + * The list of browse-entries to display */ - parent$: Observable>; + browseEntries$: Observable>>; /** - * The logo of the current Community or Collection + * The list of items to display when a value is present */ - logo$: Observable>; + items$: Observable>>; /** * The pagination config used to display the values @@ -98,7 +109,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { * The list of StartsWith options * Should be defined after ngOnInit is called! */ - startsWithOptions; + startsWithOptions: (string | number)[]; /** * The value we're browsing items for @@ -122,6 +133,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { */ fetchThumbnails: boolean; + /** + * Observable determining if the loading animation needs to be shown + */ + loading$ = observableOf(true); + public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, protected dsoService: DSpaceObjectDataService, @@ -130,7 +146,6 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { @Inject(APP_CONFIG) public appConfig: AppConfig, public dsoNameService: DSONameService, ) { - this.fetchThumbnails = this.appConfig.browseBy.showThumbnails; this.paginationConfig = Object.assign(new PaginationComponentOptions(), { id: BBM_PAGINATION_ID, @@ -147,12 +162,12 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( - map(([routeParams, queryParams, currentPage, currentSort]) => { - return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; + observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe( + map(([routeParams, queryParams, scope, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort]; }) - ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { - this.browseId = params.id || this.defaultBrowseId; + ).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => { + this.browseId = params.id || this.defaultBrowseId; this.authority = params.authority; if (typeof params.value === 'string'){ @@ -170,18 +185,19 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { } if (isNotEmpty(this.value)) { - this.updatePageWithItems( - browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority); + this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority); } else { - this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false)); + this.updatePage(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, false)); } - this.updateParent(params.scope); - this.updateLogo(); })); this.updateStartsWithTextOptions(); } + ngOnChanges(): void { + this.scope$.next(this.scope); + } + /** * Update the StartsWith options with text values * It adds the value "0-9" as well as all letters from A to Z @@ -200,6 +216,9 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { */ updatePage(searchOptions: BrowseEntrySearchOptions) { this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions); + this.loading$ = this.browseEntries$.pipe( + map((browseEntriesRD: RemoteData>) => browseEntriesRD.isLoading), + ); this.items$ = undefined; } @@ -214,37 +233,9 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { */ updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string, authority: string) { this.items$ = this.browseService.getBrowseItemsFor(value, authority, searchOptions); - } - - /** - * Update the parent Community or Collection using their scope - * @param scope The UUID of the Community or Collection to fetch - */ - updateParent(scope: string) { - if (hasValue(scope)) { - const linksToFollow = () => { - return [followLink('logo')]; - }; - this.parent$ = this.dsoService.findById(scope, - true, - true, - ...linksToFollow() as FollowLinkConfig[]).pipe( - getFirstSucceededRemoteData() - ); - } - } - - /** - * Update the parent Community or Collection logo - */ - updateLogo() { - if (hasValue(this.parent$)) { - this.logo$ = this.parent$.pipe( - map((rd: RemoteData) => rd.payload), - filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)), - mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo) - ); - } + this.loading$ = this.items$.pipe( + map((itemsRD: RemoteData>) => itemsRD.isLoading), + ); } /** @@ -278,7 +269,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.subs.filter((sub: Subscription) => hasValue(sub)).forEach((sub: Subscription) => sub.unsubscribe()); this.paginationService.clearPagination(this.paginationConfig.id); } @@ -307,12 +298,14 @@ export function getBrowseSearchOptions(defaultBrowseId: string, /** * Function to transform query and url parameters into searchOptions used to fetch browse entries or items * @param params URL and query parameters + * @param scope The scope to show the results * @param paginationConfig Pagination configuration * @param sortConfig Sorting configuration * @param metadata Optional metadata definition to fetch browse entries/items for * @param fetchThumbnail Optional parameter for requesting thumbnail images */ export function browseParamsToOptions(params: any, + scope: string, paginationConfig: PaginationComponentOptions, sortConfig: SortOptions, metadata?: string, @@ -322,7 +315,7 @@ export function browseParamsToOptions(params: any, paginationConfig, sortConfig, params.startsWith, - params.scope, + scope, fetchThumbnail ); } diff --git a/src/app/browse-by/browse-by-page.module.ts b/src/app/browse-by/browse-by-page.module.ts index 554a6c4f466..8a010f71056 100644 --- a/src/app/browse-by/browse-by-page.module.ts +++ b/src/app/browse-by/browse-by-page.module.ts @@ -5,12 +5,19 @@ import { ItemDataService } from '../core/data/item-data.service'; import { BrowseService } from '../core/browse/browse.service'; import { BrowseByGuard } from './browse-by-guard'; import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module'; +import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component'; +import { SharedModule } from '../shared/shared.module'; + +const DECLARATIONS = [ + BrowseByPageComponent, +]; @NgModule({ imports: [ SharedBrowseByModule, BrowseByRoutingModule, - BrowseByModule.withEntryComponents(), + BrowseByModule, + SharedModule, ], providers: [ ItemDataService, @@ -18,8 +25,11 @@ import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.modul BrowseByGuard, ], declarations: [ - - ] + ...DECLARATIONS, + ], + exports: [ + ...DECLARATIONS, + ], }) export class BrowseByPageModule { diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.html b/src/app/browse-by/browse-by-page/browse-by-page.component.html new file mode 100644 index 00000000000..6e9476e1e92 --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss b/src/app/browse-by/browse-by-page/browse-by-page.component.scss similarity index 100% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss rename to src/app/browse-by/browse-by-page/browse-by-page.component.scss diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts b/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts new file mode 100644 index 00000000000..25483028ebd --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts @@ -0,0 +1,69 @@ +// eslint-disable-next-line max-classes-per-file +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowseByPageComponent } from './browse-by-page.component'; +import { BrowseBySwitcherComponent } from '../browse-by-switcher/browse-by-switcher.component'; +import { DynamicComponentLoaderDirective } from '../../shared/abstract-component-loader/dynamic-component-loader.directive'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { ThemeService } from '../../shared/theme-support/theme.service'; +import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { Component } from '@angular/core'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { By } from '@angular/platform-browser'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; + +@rendersBrowseBy('BrowseByPageComponent' as BrowseByDataType) +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: '', + template: '', +}) +class BrowseByTestComponent { +} + +class TestBrowseByPageBrowseDefinition extends BrowseDefinition { + getRenderType(): BrowseByDataType { + return 'BrowseByPageComponent' as BrowseByDataType; + } +} + +describe('BrowseByPageComponent', () => { + let component: BrowseByPageComponent; + let fixture: ComponentFixture; + + let activatedRoute: ActivatedRouteStub; + let themeService: ThemeService; + + beforeEach(async () => { + activatedRoute = new ActivatedRouteStub(); + themeService = getMockThemeService(); + + await TestBed.configureTestingModule({ + declarations: [ + BrowseByPageComponent, + BrowseBySwitcherComponent, + DynamicComponentLoaderDirective, + ], + providers: [ + BrowseByTestComponent, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: ThemeService, useValue: themeService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BrowseByPageComponent); + component = fixture.componentInstance; + }); + + it('should create the correct browse section based on the route browseDefinition', () => { + activatedRoute.testData = { + browseDefinition: new TestBrowseByPageBrowseDefinition(), + }; + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + expect(fixture.debugElement.query(By.css('#BrowseByTestComponent'))).not.toBeNull(); + }); +}); diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.ts b/src/app/browse-by/browse-by-page/browse-by-page.component.ts new file mode 100644 index 00000000000..9df02562c60 --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; + +@Component({ + selector: 'ds-browse-by-page', + templateUrl: './browse-by-page.component.html', + styleUrls: ['./browse-by-page.component.scss'], +}) +export class BrowseByPageComponent implements OnInit { + + browseByType$: Observable; + + constructor( + protected route: ActivatedRoute, + ) { + } + + /** + * Fetch the correct browse-by component by using the relevant config from the route data + */ + ngOnInit(): void { + this.browseByType$ = this.route.data.pipe( + map((data: { browseDefinition: BrowseDefinition }) => data.browseDefinition.getRenderType()), + ); + } + +} diff --git a/src/app/browse-by/browse-by-routing.module.ts b/src/app/browse-by/browse-by-routing.module.ts index bb67dc65aed..f07df26b32b 100644 --- a/src/app/browse-by/browse-by-routing.module.ts +++ b/src/app/browse-by/browse-by-routing.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { BrowseByGuard } from './browse-by-guard'; import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; -import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component'; +import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; @NgModule({ @@ -18,7 +18,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; children: [ { path: ':id', - component: ThemedBrowseBySwitcherComponent, + component: BrowseByPageComponent, canActivate: [BrowseByGuard], resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' } diff --git a/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts b/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts new file mode 100644 index 00000000000..5324018b346 --- /dev/null +++ b/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts @@ -0,0 +1,6 @@ +export enum BrowseByDataType { + Title = 'title', + Metadata = 'text', + Date = 'date', + Hierarchy = 'hierarchy', +} diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts index 19a6277151c..64604cdc04a 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts @@ -1,4 +1,5 @@ -import { BrowseByDataType, rendersBrowseBy } from './browse-by-decorator'; +import { BrowseByDataType } from './browse-by-data-type'; +import { rendersBrowseBy } from './browse-by-decorator'; describe('BrowseByDecorator', () => { const titleDecorator = rendersBrowseBy(BrowseByDataType.Title); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts index b59a46cae11..62e666227d4 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts @@ -1,40 +1,36 @@ +import { Component } from '@angular/core'; import { hasNoValue } from '../../shared/empty.util'; -import { InjectionToken } from '@angular/core'; +import { DEFAULT_THEME, resolveTheme } from '../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../core/shared/context.model'; import { GenericConstructor } from '../../core/shared/generic-constructor'; -import { - DEFAULT_THEME, - resolveTheme -} from '../../shared/object-collection/shared/listable-object/listable-object.decorator'; - -export enum BrowseByDataType { - Title = 'title', - Metadata = 'text', - Date = 'date' -} +import { BrowseByDataType } from './browse-by-data-type'; export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata; +export const DEFAULT_BROWSE_BY_CONTEXT = Context.Any; -export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType, theme) => GenericConstructor>('getComponentByBrowseByType', { - providedIn: 'root', - factory: () => getComponentByBrowseByType -}); - -const map = new Map(); +const map: Map>>> = new Map(); /** * Decorator used for rendering Browse-By pages by type * @param browseByType The type of page + * @param context The optional context for the component * @param theme The optional theme for the component */ -export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) { +export function rendersBrowseBy(browseByType: BrowseByDataType, context = DEFAULT_BROWSE_BY_CONTEXT, theme = DEFAULT_THEME) { return function decorator(component: any) { + if (hasNoValue(browseByType)) { + return; + } if (hasNoValue(map.get(browseByType))) { map.set(browseByType, new Map()); } - if (hasNoValue(map.get(browseByType).get(theme))) { - map.get(browseByType).set(theme, component); + if (hasNoValue(map.get(browseByType).get(context))) { + map.get(browseByType).set(context, new Map()); + } + if (hasNoValue(map.get(browseByType).get(context).get(theme))) { + map.get(browseByType).get(context).set(theme, component); } else { - throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}" and theme "${theme}"`); + throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}", context "${context}" and theme "${theme}"`); } }; } @@ -42,12 +38,17 @@ export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) { /** * Get the component used for rendering a Browse-By page by type * @param browseByType The type of page + * @param context The context to match * @param theme the theme to match */ -export function getComponentByBrowseByType(browseByType, theme) { - let themeMap = map.get(browseByType); +export function getComponentByBrowseByType(browseByType: BrowseByDataType, context: Context, theme: string): GenericConstructor { + let contextMap: Map>> = map.get(browseByType); + if (hasNoValue(contextMap)) { + contextMap = map.get(DEFAULT_BROWSE_BY_TYPE); + } + let themeMap: Map> = contextMap.get(context); if (hasNoValue(themeMap)) { - themeMap = map.get(DEFAULT_BROWSE_BY_TYPE); + themeMap = contextMap.get(DEFAULT_BROWSE_BY_CONTEXT); } const comp = resolveTheme(themeMap, theme); if (hasNoValue(comp)) { diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html deleted file mode 100644 index afe79cf2b10..00000000000 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts index c13405dd4d2..65e8c87c619 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts @@ -1,13 +1,23 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator'; -import { BehaviorSubject } from 'rxjs'; +import { SimpleChange, Component } from '@angular/core'; +import { rendersBrowseBy } from './browse-by-decorator'; import { ThemeService } from '../../shared/theme-support/theme.service'; import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model'; import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model'; import { NonHierarchicalBrowseDefinition } from '../../core/shared/non-hierarchical-browse-definition'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { DynamicComponentLoaderDirective } from '../../shared/abstract-component-loader/dynamic-component-loader.directive'; +import { BrowseByDataType } from './browse-by-data-type'; + +@rendersBrowseBy('BrowseBySwitcherComponent' as BrowseByDataType) +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: '', + template: '', +}) +class BrowseByTestComponent { +} describe('BrowseBySwitcherComponent', () => { let comp: BrowseBySwitcherComponent; @@ -41,46 +51,45 @@ describe('BrowseBySwitcherComponent', () => { ), ]; - const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition())); - - const activatedRouteStub = { - data - }; - let themeService: ThemeService; - let themeName: string; + const themeName = 'dspace'; beforeEach(waitForAsync(() => { - themeName = 'dspace'; - themeService = jasmine.createSpyObj('themeService', { - getThemeName: themeName, - }); + themeService = getMockThemeService(themeName); - TestBed.configureTestingModule({ - declarations: [BrowseBySwitcherComponent], + void TestBed.configureTestingModule({ + declarations: [ + BrowseBySwitcherComponent, + DynamicComponentLoaderDirective, + ], providers: [ - { provide: ActivatedRoute, useValue: activatedRouteStub }, + BrowseByTestComponent, { provide: ThemeService, useValue: themeService }, - { provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) } ], - schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(BrowseBySwitcherComponent); comp = fixture.componentInstance; + spyOn(comp, 'getComponent').and.returnValue(BrowseByTestComponent); + spyOn(comp, 'connectInputsAndOutputs').and.callThrough(); })); types.forEach((type: NonHierarchicalBrowseDefinition) => { describe(`when switching to a browse-by page for "${type.id}"`, () => { - beforeEach(() => { - data.next(createDataWithBrowseDefinition(type)); + beforeEach(async () => { + comp.browseByType = type.dataType; + comp.ngOnChanges({ + browseByType: new SimpleChange(undefined, type.dataType, true), + }); fixture.detectChanges(); + await fixture.whenStable(); }); - it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => { - expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType, themeName); + it(`should call getComponent with type "${type.dataType}"`, () => { + expect(comp.getComponent).toHaveBeenCalled(); + expect(comp.connectInputsAndOutputs).toHaveBeenCalled(); }); }); }); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 35e4edf9005..99b969417e6 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -1,38 +1,38 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; +import { Component, Input } from '@angular/core'; +import { getComponentByBrowseByType } from './browse-by-decorator'; import { GenericConstructor } from '../../core/shared/generic-constructor'; -import { BrowseDefinition } from '../../core/shared/browse-definition.model'; -import { ThemeService } from '../../shared/theme-support/theme.service'; +import { AbstractComponentLoaderComponent } from '../../shared/abstract-component-loader/abstract-component-loader.component'; +import { BrowseByDataType } from './browse-by-data-type'; +import { Context } from '../../core/shared/context.model'; @Component({ selector: 'ds-browse-by-switcher', - templateUrl: './browse-by-switcher.component.html' + templateUrl: '../../shared/abstract-component-loader/abstract-component-loader.component.html' }) -/** - * Component for determining what Browse-By component to use depending on the metadata (browse ID) provided - */ -export class BrowseBySwitcherComponent implements OnInit { - - /** - * Resolved browse-by component - */ - browseByComponent: Observable; - - public constructor(protected route: ActivatedRoute, - protected themeService: ThemeService, - @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor) { - } +export class BrowseBySwitcherComponent extends AbstractComponentLoaderComponent { + + @Input() context: Context; + + @Input() browseByType: BrowseByDataType; + + @Input() displayTitle: boolean; + + @Input() scope: string; + + protected inputNamesDependentForComponent: (keyof this & string)[] = [ + 'context', + 'browseByType', + ]; + + protected inputNames: (keyof this & string)[] = [ + 'context', + 'browseByType', + 'displayTitle', + 'scope', + ]; - /** - * Fetch the correct browse-by component by using the relevant config from the route data - */ - ngOnInit(): void { - this.browseByComponent = this.route.data.pipe( - map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName())) - ); + public getComponent(): GenericConstructor { + return getComponentByBrowseByType(this.browseByType, this.context, this.themeService.getThemeName()); } } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts b/src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts similarity index 63% rename from src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts rename to src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts index e569f6cc6f8..8c77df1cdb8 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts +++ b/src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts @@ -1,12 +1,12 @@ import { Directive, ViewContainerRef } from '@angular/core'; -@Directive({ - selector: '[dsAdvancedWorkflowActions]', -}) /** - * Directive used as a hook to know where to inject the dynamic Advanced Claimed Task Actions component + * Directive used as a hook to know where to inject the dynamic loaded component */ -export class AdvancedWorkflowActionsDirective { +@Directive({ + selector: '[dsDynamicComponentLoader]' +}) +export class DynamicComponentLoaderDirective { constructor( public viewContainerRef: ViewContainerRef, diff --git a/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts deleted file mode 100644 index 0187d4e3c5e..00000000000 --- a/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core'; - -import { ThemedComponent } from '../../shared/theme-support/themed.component'; -import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; - -/** - * Themed wrapper for BrowseBySwitcherComponent - */ -@Component({ - selector: 'ds-themed-browse-by-switcher', - styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html' -}) -export class ThemedBrowseBySwitcherComponent extends ThemedComponent { - protected getComponentName(): string { - return 'BrowseBySwitcherComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-switcher/browse-by-switcher.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-switcher.component`); - } - - -} diff --git a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts deleted file mode 100644 index 212044b8539..00000000000 --- a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core'; -import { ThemedComponent } from '../../shared/theme-support/themed.component'; -import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; -import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component'; - -@Component({ - selector: 'ds-themed-browse-by-taxonomy-page', - templateUrl: '../../shared/theme-support/themed.component.html', - styleUrls: [] -}) -/** - * Themed wrapper for BrowseByTaxonomyPageComponent - */ -@rendersBrowseBy('hierarchy') -export class ThemedBrowseByTaxonomyPageComponent extends ThemedComponent{ - - protected getComponentName(): string { - return 'BrowseByTaxonomyPageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-taxonomy-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html similarity index 70% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html index c24ca934032..3dd9b6b25a6 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html @@ -1,5 +1,11 @@ -
-

{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }}

+
+

+ {{ ('browse.title') | translate:{ + field: 'browse.metadata.' + vocabularyName | translate, + startsWith: '', + value: '', + } }} +

{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }} [queryParams]="queryParams" [queryParamsHandling]="'merge'"> {{ 'browse.taxonomy.button' | translate }} -
+
diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss similarity index 100% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts similarity index 68% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts index c724017b1fd..ac8dbff5982 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component'; +import { BrowseByTaxonomyComponent } from './browse-by-taxonomy.component'; import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { TranslateModule } from '@ngx-translate/core'; import { NO_ERRORS_SCHEMA } from '@angular/core'; @@ -10,9 +9,9 @@ import { createDataWithBrowseDefinition } from '../browse-by-switcher/browse-by- import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model'; import { ThemeService } from '../../shared/theme-support/theme.service'; -describe('BrowseByTaxonomyPageComponent', () => { - let component: BrowseByTaxonomyPageComponent; - let fixture: ComponentFixture; +describe('BrowseByTaxonomyComponent', () => { + let component: BrowseByTaxonomyComponent; + let fixture: ComponentFixture; let themeService: ThemeService; let detail1: VocabularyEntryDetail; let detail2: VocabularyEntryDetail; @@ -29,7 +28,9 @@ describe('BrowseByTaxonomyPageComponent', () => { await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot() ], - declarations: [ BrowseByTaxonomyPageComponent ], + declarations: [ + BrowseByTaxonomyComponent, + ], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: ThemeService, useValue: themeService }, @@ -40,8 +41,9 @@ describe('BrowseByTaxonomyPageComponent', () => { }); beforeEach(() => { - fixture = TestBed.createComponent(BrowseByTaxonomyPageComponent); + fixture = TestBed.createComponent(BrowseByTaxonomyComponent); component = fixture.componentInstance; + spyOn(component, 'updateQueryParams').and.callThrough(); fixture.detectChanges(); detail1 = new VocabularyEntryDetail(); detail2 = new VocabularyEntryDetail(); @@ -61,6 +63,7 @@ describe('BrowseByTaxonomyPageComponent', () => { expect(component.selectedItems).toContain(detail1); expect(component.selectedItems.length).toBe(1); expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals'] ); + expect(component.updateQueryParams).toHaveBeenCalled(); }); it('should handle select event with multiple selected items', () => { @@ -70,6 +73,7 @@ describe('BrowseByTaxonomyPageComponent', () => { expect(component.selectedItems).toContain(detail1, detail2); expect(component.selectedItems.length).toBe(2); expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'] ); + expect(component.updateQueryParams).toHaveBeenCalled(); }); it('should handle deselect event', () => { @@ -82,6 +86,33 @@ describe('BrowseByTaxonomyPageComponent', () => { expect(component.selectedItems).toContain(detail2); expect(component.selectedItems.length).toBe(1); expect(component.filterValues).toEqual(['TECHNOLOGY,equals'] ); + expect(component.updateQueryParams).toHaveBeenCalled(); + }); + + describe('updateQueryParams', () => { + beforeEach(() => { + component.facetType = 'subject'; + component.filterValues = ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals']; + }); + + it('should update the queryParams with the selected filterValues', () => { + component.updateQueryParams(); + + expect(component.queryParams).toEqual({ + 'f.subject': ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'], + }); + }); + + it('should include the scope if present', () => { + component.scope = '67f849f1-2499-4872-8c61-9e2b47d71068'; + + component.updateQueryParams(); + + expect(component.queryParams).toEqual({ + 'f.subject': ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'], + 'scope': '67f849f1-2499-4872-8c61-9e2b47d71068', + }); + }); }); afterEach(() => { diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts similarity index 50% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts index cf6345bf394..d23a3dd15c2 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts @@ -1,24 +1,48 @@ -import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnChanges, OnDestroy, Input } from '@angular/core'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; -import { ActivatedRoute } from '@angular/router'; -import { Observable, Subscription } from 'rxjs'; +import { ActivatedRoute, Params } from '@angular/router'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BrowseDefinition } from '../../core/shared/browse-definition.model'; -import { GenericConstructor } from '../../core/shared/generic-constructor'; -import { BROWSE_BY_COMPONENT_FACTORY } from '../browse-by-switcher/browse-by-decorator'; +import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { map } from 'rxjs/operators'; -import { ThemeService } from 'src/app/shared/theme-support/theme.service'; import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; +import { Context } from '../../core/shared/context.model'; +import { hasValue } from '../../shared/empty.util'; @Component({ - selector: 'ds-browse-by-taxonomy-page', - templateUrl: './browse-by-taxonomy-page.component.html', - styleUrls: ['./browse-by-taxonomy-page.component.scss'] + selector: 'ds-browse-by-taxonomy', + templateUrl: './browse-by-taxonomy.component.html', + styleUrls: ['./browse-by-taxonomy.component.scss'], }) /** * Component for browsing items by metadata in a hierarchical controlled vocabulary */ -export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy { +@rendersBrowseBy(BrowseByDataType.Hierarchy) +export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy { + + /** + * The optional context + */ + @Input() context: Context; + + /** + * The {@link BrowseByDataType} of this Component + */ + @Input() browseByType: BrowseByDataType; + + /** + * The ID of the {@link Community} or {@link Collection} of the scope to display + */ + @Input() scope: string; + + /** + * Display the h1 title in the section + */ + @Input() displayTitle = true; + + scope$: BehaviorSubject = new BehaviorSubject(undefined); /** * The {@link VocabularyOptions} object @@ -48,35 +72,41 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy { /** * The parameters used in the URL */ - queryParams: any; + queryParams: Params; /** - * Resolved browse-by component + * Resolved browse-by definition */ - browseByComponent: Observable; + browseDefinition$: Observable; /** * Subscriptions to track */ - browseByComponentSubs: Subscription[] = []; + subs: Subscription[] = []; - public constructor( protected route: ActivatedRoute, - protected themeService: ThemeService, - @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor) { + public constructor( + protected route: ActivatedRoute, + ) { } ngOnInit(): void { - this.browseByComponent = this.route.data.pipe( + this.browseDefinition$ = this.route.data.pipe( map((data: { browseDefinition: BrowseDefinition }) => { - this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName()); return data.browseDefinition; }) ); - this.browseByComponentSubs.push(this.browseByComponent.subscribe((browseDefinition: HierarchicalBrowseDefinition) => { + this.subs.push(this.browseDefinition$.subscribe((browseDefinition: HierarchicalBrowseDefinition) => { this.facetType = browseDefinition.facetType; this.vocabularyName = browseDefinition.vocabulary; this.vocabularyOptions = { name: this.vocabularyName, closed: true }; })); + this.subs.push(this.scope$.subscribe(() => { + this.updateQueryParams(); + })); + } + + ngOnChanges(): void { + this.scope$.next(this.scope); } /** @@ -86,9 +116,9 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy { * @param detail VocabularyEntryDetail to be added */ onSelect(detail: VocabularyEntryDetail): void { - this.selectedItems.push(detail); - this.filterValues = this.selectedItems - .map((item: VocabularyEntryDetail) => `${item.value},equals`); + this.selectedItems.push(detail); + this.filterValues = this.selectedItems + .map((item: VocabularyEntryDetail) => `${item.value},equals`); this.updateQueryParams(); } @@ -98,21 +128,28 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy { * @param detail VocabularyEntryDetail to be removed */ onDeselect(detail: VocabularyEntryDetail): void { - this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { return entry.id !== detail.id; }); - this.filterValues = this.filterValues.filter((value: string) => { return value !== `${detail.value},equals`; }); + this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { + return entry.id !== detail.id; + }); + this.filterValues = this.filterValues.filter((value: string) => { + return value !== `${detail.value},equals`; + }); this.updateQueryParams(); } /** * Updates queryParams based on the current facetType and filterValues. */ - private updateQueryParams(): void { + updateQueryParams(): void { this.queryParams = { ['f.' + this.facetType]: this.filterValues }; + if (hasValue(this.scope)) { + this.queryParams.scope = this.scope; + } } ngOnDestroy(): void { - this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe()); + this.subs.forEach((sub: Subscription) => sub.unsubscribe()); } } diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts deleted file mode 100644 index 58df79ebe85..00000000000 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { combineLatest as observableCombineLatest } from 'rxjs'; -import { Component, Inject } from '@angular/core'; -import { ActivatedRoute, Params, Router } from '@angular/router'; -import { hasValue } from '../../shared/empty.util'; -import { - BrowseByMetadataPageComponent, - browseParamsToOptions, getBrowseSearchOptions -} from '../browse-by-metadata-page/browse-by-metadata-page.component'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { BrowseService } from '../../core/browse/browse.service'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { map } from 'rxjs/operators'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; - -@Component({ - selector: 'ds-browse-by-title-page', - styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'], - templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html' -}) -/** - * Component for browsing items by title (dc.title) - */ -export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { - - public constructor(protected route: ActivatedRoute, - protected browseService: BrowseService, - protected dsoService: DSpaceObjectDataService, - protected paginationService: PaginationService, - protected router: Router, - @Inject(APP_CONFIG) public appConfig: AppConfig, - public dsoNameService: DSONameService, - ) { - super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService); - } - - ngOnInit(): void { - const sortConfig = new SortOptions('dc.title', SortDirection.ASC); - // include the thumbnail configuration in browse search options - this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails)); - this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); - this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); - this.subs.push( - observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( - map(([routeParams, queryParams, currentPage, currentSort]) => { - return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; - }) - ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { - this.startsWith = +params.startsWith || params.startsWith; - this.browseId = params.id || this.defaultBrowseId; - this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined); - this.updateParent(params.scope); - this.updateLogo(); - })); - this.updateStartsWithTextOptions(); - } - - ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); - } - -} diff --git a/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts deleted file mode 100644 index 4a1bcc0bc11..00000000000 --- a/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Component} from '@angular/core'; -import { ThemedComponent } from '../../shared/theme-support/themed.component'; -import { BrowseByTitlePageComponent } from './browse-by-title-page.component'; -import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator'; - -/** - * Themed wrapper for BrowseByTitlePageComponent - */ -@Component({ - selector: 'ds-themed-browse-by-title-page', - styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', -}) - -@rendersBrowseBy(BrowseByDataType.Title) -export class ThemedBrowseByTitlePageComponent - extends ThemedComponent { - protected getComponentName(): string { - return 'BrowseByTitlePageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-title-page/browse-by-title-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-title-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts similarity index 87% rename from src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts rename to src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts index e32c0ac430d..54394087ec3 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts +++ b/src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts @@ -9,8 +9,8 @@ import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec'; -import { BrowseByTitlePageComponent } from './browse-by-title-page.component'; +import { toRemoteData } from '../browse-by-metadata/browse-by-metadata.component.spec'; +import { BrowseByTitleComponent } from './browse-by-title.component'; import { ItemDataService } from '../../core/data/item-data.service'; import { Community } from '../../core/shared/community.model'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; @@ -24,9 +24,9 @@ import { APP_CONFIG } from '../../../config/app-config.interface'; import { environment } from '../../../environments/environment'; -describe('BrowseByTitlePageComponent', () => { - let comp: BrowseByTitlePageComponent; - let fixture: ComponentFixture; +describe('BrowseByTitleComponent', () => { + let comp: BrowseByTitleComponent; + let fixture: ComponentFixture; let itemDataService: ItemDataService; let route: ActivatedRoute; @@ -71,7 +71,7 @@ describe('BrowseByTitlePageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BrowseByTitlePageComponent, EnumKeysPipe, VarDirective], + declarations: [BrowseByTitleComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: BrowseService, useValue: mockBrowseService }, @@ -85,7 +85,7 @@ describe('BrowseByTitlePageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(BrowseByTitlePageComponent); + fixture = TestBed.createComponent(BrowseByTitleComponent); comp = fixture.componentInstance; fixture.detectChanges(); itemDataService = (comp as any).itemDataService; diff --git a/src/app/browse-by/browse-by-title/browse-by-title.component.ts b/src/app/browse-by/browse-by-title/browse-by-title.component.ts new file mode 100644 index 00000000000..7b603af48bc --- /dev/null +++ b/src/app/browse-by/browse-by-title/browse-by-title.component.ts @@ -0,0 +1,44 @@ +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; +import { Params } from '@angular/router'; +import { + BrowseByMetadataComponent, + browseParamsToOptions, getBrowseSearchOptions +} from '../browse-by-metadata/browse-by-metadata.component'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { map } from 'rxjs/operators'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; + +@Component({ + selector: 'ds-browse-by-title', + styleUrls: ['../browse-by-metadata/browse-by-metadata.component.scss'], + templateUrl: '../browse-by-metadata/browse-by-metadata.component.html' +}) +/** + * Component for browsing items by title (dc.title) + */ +@rendersBrowseBy(BrowseByDataType.Title) +export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit { + + ngOnInit(): void { + const sortConfig = new SortOptions('dc.title', SortDirection.ASC); + // include the thumbnail configuration in browse search options + this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails)); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); + this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); + this.subs.push( + observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe( + map(([routeParams, queryParams, scope, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort]; + }) + ).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => { + this.startsWith = +params.startsWith || params.startsWith; + this.browseId = params.id || this.defaultBrowseId; + this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined); + })); + this.updateStartsWithTextOptions(); + } + +} diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index ec9f22347f7..b1263595b31 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -1,50 +1,42 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component'; -import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component'; -import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component'; +import { BrowseByTitleComponent } from './browse-by-title/browse-by-title.component'; +import { BrowseByMetadataComponent } from './browse-by-metadata/browse-by-metadata.component'; +import { BrowseByDateComponent } from './browse-by-date/browse-by-date.component'; import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component'; -import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component'; -import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component'; -import { ComcolModule } from '../shared/comcol/comcol.module'; -import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component'; -import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component'; -import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component'; -import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component'; +import { BrowseByTaxonomyComponent } from './browse-by-taxonomy/browse-by-taxonomy.component'; import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; import { FormModule } from '../shared/form/form.module'; import { SharedModule } from '../shared/shared.module'; +const DECLARATIONS = [ + BrowseBySwitcherComponent, +]; + const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator - BrowseByTitlePageComponent, - BrowseByMetadataPageComponent, - BrowseByDatePageComponent, - BrowseByTaxonomyPageComponent, - - ThemedBrowseByMetadataPageComponent, - ThemedBrowseByDatePageComponent, - ThemedBrowseByTitlePageComponent, - ThemedBrowseByTaxonomyPageComponent, + BrowseByTitleComponent, + BrowseByMetadataComponent, + BrowseByDateComponent, + BrowseByTaxonomyComponent, ]; @NgModule({ imports: [ SharedBrowseByModule, CommonModule, - ComcolModule, DsoPageModule, FormModule, SharedModule, ], declarations: [ - BrowseBySwitcherComponent, - ThemedBrowseBySwitcherComponent, + ...DECLARATIONS, ...ENTRY_COMPONENTS ], exports: [ - BrowseBySwitcherComponent + ...DECLARATIONS, + ...ENTRY_COMPONENTS, ] }) export class BrowseByModule { diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 9dc25b778ee..5ddef6ca68b 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -22,6 +22,10 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; +import { BrowseByGuard } from '../browse-by/browse-by-guard'; +import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; +import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component'; @NgModule({ imports: [ @@ -65,7 +69,23 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; { path: '', component: ThemedCollectionPageComponent, - pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + component: CollectionRecentlyAddedComponent, + }, + { + path: 'browse/:id', + pathMatch: 'full', + component: ComcolBrowseByComponent, + canActivate: [BrowseByGuard], + resolve: { + breadcrumb: BrowseByI18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'browse.metadata' }, + }, + ], } ], data: { diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 3cbd08f1325..21cc94af68d 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -1,78 +1,61 @@
-
-
-
- -
-
- - - - - - +
+
+
+ +
+
+ + + + + + - - - - - - - - - -
- -
-
- - - + + + + + + + + + +
+ +
+
+ + + - -
-

{{'collection.page.browse.recent.head' | translate}}

- - -
- - - -
-
-
+ + +
+ [content]="collection.copyrightText" + [hasInnerHtml]="true">
- - + +
diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index 16704cef52e..6b3cbbe64eb 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -1,36 +1,21 @@ -import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs'; -import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model'; -import { SearchService } from '../core/shared/search/search.service'; -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 { Observable } from 'rxjs'; +import { filter, map, mergeMap, take } from 'rxjs/operators'; +import { SortOptions } from '../core/cache/models/sort-options.model'; import { RemoteData } from '../core/data/remote-data'; import { Bitstream } from '../core/shared/bitstream.model'; - import { Collection } from '../core/shared/collection.model'; -import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; -import { Item } from '../core/shared/item.model'; -import { - getAllSucceededRemoteDataPayload, - getFirstSucceededRemoteData, - toDSpaceObjectListRD -} from '../core/shared/operators'; - +import { getAllSucceededRemoteDataPayload } from '../core/shared/operators'; import { fadeIn, fadeInOut } from '../shared/animations/fade'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { AuthService } from '../core/auth/auth.service'; -import { PaginationService } from '../core/pagination/pagination.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { getCollectionPageRoute } from './collection-page-routing-paths'; import { redirectOn4xx } from '../core/shared/authorized.operators'; -import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service'; import { DSONameService } from '../core/breadcrumbs/dso-name.service'; -import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface'; @Component({ selector: 'ds-collection-page', @@ -44,14 +29,9 @@ import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface' }) export class CollectionPageComponent implements OnInit { collectionRD$: Observable>; - itemRD$: Observable>>; logoRD$: Observable>; paginationConfig: PaginationComponentOptions; sortConfig: SortOptions; - private paginationChanges$: Subject<{ - paginationConfig: PaginationComponentOptions, - sortConfig: SortOptions - }>; /** * Whether the current user is a Community admin @@ -64,23 +44,12 @@ export class CollectionPageComponent implements OnInit { collectionPageRoute$: Observable; constructor( - private collectionDataService: CollectionDataService, - private searchService: SearchService, - private route: ActivatedRoute, - private router: Router, - private authService: AuthService, - private paginationService: PaginationService, - private authorizationDataService: AuthorizationDataService, + protected route: ActivatedRoute, + protected router: Router, + protected authService: AuthService, + protected authorizationDataService: AuthorizationDataService, public dsoNameService: DSONameService, - @Inject(APP_CONFIG) public appConfig: AppConfig, ) { - this.paginationConfig = Object.assign(new PaginationComponentOptions(), { - id: 'cp', - currentPage: 1, - pageSize: this.appConfig.browseBy.pageSize, - }); - - this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC); } ngOnInit(): void { @@ -96,33 +65,6 @@ export class CollectionPageComponent implements OnInit { ); this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin); - this.paginationChanges$ = new BehaviorSubject({ - paginationConfig: this.paginationConfig, - sortConfig: this.sortConfig - }); - - const currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); - const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig); - - this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe( - switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe( - getFirstSucceededRemoteData(), - map((rd) => rd.payload.id), - switchMap((id: string) => { - return this.searchService.search( - new PaginatedSearchOptions({ - scope: id, - pagination: currentPagination, - sort: currentSort, - dsoTypes: [DSpaceObjectType.ITEM] - }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW) - .pipe(toDSpaceObjectListRD()) as Observable>>; - }), - startWith(undefined) // Make sure switching pages shows loading component - ) - ) - ); - this.collectionPageRoute$ = this.collectionRD$.pipe( getAllSucceededRemoteDataPayload(), map((collection) => getCollectionPageRoute(collection.id)) @@ -133,9 +75,5 @@ export class CollectionPageComponent implements OnInit { return isNotEmpty(object); } - ngOnDestroy(): void { - this.paginationService.clearPagination(this.paginationConfig.id); - } - } diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index 6bcefed2b72..8782be0a45f 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -18,6 +18,19 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen import { ComcolModule } from '../shared/comcol/comcol.module'; import { DsoSharedModule } from '../dso-shared/dso-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; +import { BrowseByPageModule } from '../browse-by/browse-by-page.module'; +import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component'; + +const DECLARATIONS = [ + CollectionPageComponent, + ThemedCollectionPageComponent, + CreateCollectionPageComponent, + DeleteCollectionPageComponent, + EditItemTemplatePageComponent, + ThemedEditItemTemplatePageComponent, + CollectionItemMapperComponent, + CollectionRecentlyAddedComponent, +]; @NgModule({ imports: [ @@ -30,15 +43,10 @@ import { DsoPageModule } from '../shared/dso-page/dso-page.module'; ComcolModule, DsoSharedModule, DsoPageModule, + BrowseByPageModule, ], declarations: [ - CollectionPageComponent, - ThemedCollectionPageComponent, - CreateCollectionPageComponent, - DeleteCollectionPageComponent, - EditItemTemplatePageComponent, - ThemedEditItemTemplatePageComponent, - CollectionItemMapperComponent + ...DECLARATIONS, ], providers: [ SearchService, diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index ffd8f713436..1cf40159ec9 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,17 +1,17 @@
- + {{ 'collection.edit.template.label' | translate}}
diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.html b/src/app/collection-page/sections/recently-added/collection-recently-added.component.html new file mode 100644 index 00000000000..002b8cceda7 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.html @@ -0,0 +1,18 @@ + +
+

{{'collection.page.browse.recent.head' | translate}}

+ + +
+ + + +
diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.scss b/src/app/collection-page/sections/recently-added/collection-recently-added.component.scss similarity index 100% rename from src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.scss rename to src/app/collection-page/sections/recently-added/collection-recently-added.component.scss diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts new file mode 100644 index 00000000000..4acc24e3f59 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CollectionRecentlyAddedComponent } from './collection-recently-added.component'; +import { APP_CONFIG } from '../../../../config/app-config.interface'; +import { environment } from '../../../../environments/environment.test'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { SearchServiceStub } from '../../../shared/testing/search-service.stub'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + +describe('CollectionRecentlyAddedComponent', () => { + let component: CollectionRecentlyAddedComponent; + let fixture: ComponentFixture; + + let activatedRoute: ActivatedRouteStub; + let paginationService: PaginationServiceStub; + let searchService: SearchServiceStub; + + beforeEach(async () => { + activatedRoute = new ActivatedRouteStub(); + paginationService = new PaginationServiceStub(); + searchService = new SearchServiceStub(); + + await TestBed.configureTestingModule({ + declarations: [ + CollectionRecentlyAddedComponent, + VarDirective, + ], + imports: [ + TranslateModule.forRoot(), + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: APP_CONFIG, useValue: environment }, + { provide: PaginationService, useValue: paginationService }, + { provide: SearchService, useValue: SearchServiceStub }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionRecentlyAddedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts new file mode 100644 index 00000000000..65af77a63b5 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts @@ -0,0 +1,82 @@ +import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; +import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { Item } from '../../../core/shared/item.model'; +import { switchMap, map, startWith, take } from 'rxjs/operators'; +import { getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; +import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; +import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { BROWSE_LINKS_TO_FOLLOW } from '../../../core/browse/browse.service'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortOptions, SortDirection } from '../../../core/cache/models/sort-options.model'; +import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { ActivatedRoute, Data } from '@angular/router'; +import { fadeIn } from '../../../shared/animations/fade'; + +@Component({ + selector: 'ds-collection-recently-added', + templateUrl: './collection-recently-added.component.html', + styleUrls: ['./collection-recently-added.component.scss'], + animations: [fadeIn], +}) +export class CollectionRecentlyAddedComponent implements OnInit, OnDestroy { + + paginationConfig: PaginationComponentOptions; + + sortConfig: SortOptions; + + collectionRD$: Observable>; + + itemRD$: Observable>>; + + constructor( + @Inject(APP_CONFIG) protected appConfig: AppConfig, + protected paginationService: PaginationService, + protected route: ActivatedRoute, + protected searchService: SearchService, + ) { + this.paginationConfig = Object.assign(new PaginationComponentOptions(), { + id: 'cp', + currentPage: 1, + pageSize: this.appConfig.browseBy.pageSize, + }); + + this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC); + } + + ngOnInit(): void { + this.collectionRD$ = this.route.data.pipe( + map((data: Data) => data.dso as RemoteData), + take(1), + ); + + this.itemRD$ = observableCombineLatest([ + this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig), + this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig), + ]).pipe( + switchMap(([currentPagination, currentSort]: [PaginationComponentOptions, SortOptions]) => this.collectionRD$.pipe( + getFirstSucceededRemoteData(), + map((rd: RemoteData) => rd.payload.id), + switchMap((id: string) => this.searchService.search( + new PaginatedSearchOptions({ + scope: id, + pagination: currentPagination, + sort: currentSort, + dsoTypes: [DSpaceObjectType.ITEM] + }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe( + toDSpaceObjectListRD() + ) as Observable>>), + startWith(undefined), // Make sure switching pages shows loading component + )), + ); + } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.paginationConfig.id); + } + +} diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index eed9ca64cad..7ccf24a7619 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -4,9 +4,9 @@
- +
+ +
{{ dsoNameService.getName(node.payload) }} @@ -44,10 +47,9 @@
- + {{node.payload.shortDescription}} @@ -56,10 +58,9 @@
- +
@@ -67,9 +68,9 @@
- +
{{ dsoNameService.getName(node.payload) }}
@@ -77,10 +78,9 @@
- + {{node.payload.shortDescription}} diff --git a/src/app/community-list-page/community-list/community-list.component.scss b/src/app/community-list-page/community-list/community-list.component.scss new file mode 100644 index 00000000000..2e33380a29f --- /dev/null +++ b/src/app/community-list-page/community-list/community-list.component.scss @@ -0,0 +1,4 @@ +::ng-deep .fa-chevron-right::before { + display: block; + width: 16px; +} diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index fb47f4994d2..cec1b555ab8 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -5,7 +5,7 @@ import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community import { CdkTreeModule } from '@angular/cdk/tree'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { Community } from '../../core/shared/community.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -300,12 +300,14 @@ describe('CommunityListComponent', () => { describe('second top community node is expanded and has more children (collections) than page size of collection', () => { describe('children of second top com are added (page-limited pageSize 2)', () => { - let allNodes; + let allNodes: DebugElement[]; beforeEach(fakeAsync(() => { - const chevronExpand = fixture.debugElement.queryAll(By.css('.expandable-node button')); - const chevronExpandSpan = fixture.debugElement.queryAll(By.css('.expandable-node button span')); - if (chevronExpandSpan[1].nativeElement.classList.contains('fa-chevron-right')) { - chevronExpand[1].nativeElement.click(); + const toggleButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.expandable-node button')); + const toggleButtonText: DebugElement = toggleButtons[1].query(By.css('span')); + expect(toggleButtonText).not.toBeNull(); + + if (toggleButtonText.nativeElement.classList.contains('fa-chevron-right')) { + toggleButtons[1].nativeElement.click(); tick(); fixture.detectChanges(); } @@ -315,17 +317,18 @@ describe('CommunityListComponent', () => { allNodes = [...expandableNodesFound, ...childlessNodesFound]; })); it('tree contains 2 (page-limited) top com, 2 (page-limited) coll of 2nd top com, a show more for those page-limited coll and show more for page-limited top com', () => { - mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === topFlatnode.name); - })).toBeTruthy(); - }); - mockCollectionsPage1.map((coll) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === coll.name); - })).toBeTruthy(); - }); + const allNodeNames: string[] = allNodes.map((node: DebugElement) => node.nativeElement.innerText.trim()); expect(allNodes.length).toEqual(4); + const flatNodes: string[] = mockTopFlatnodesUnexpanded.slice(0, 2).map((flatNode: FlatNode) => flatNode.name); + for (const flatNode of flatNodes) { + expect(allNodeNames).toContain(flatNode); + } + expect(flatNodes.length).toBe(2); + const page1CollectionNames: string[] = mockCollectionsPage1.map((collection: Collection) => collection.name); + for (const collectionName of page1CollectionNames) { + expect(allNodeNames).toContain(collectionName); + } + expect(page1CollectionNames.length).toBe(2); const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node')); expect(showMoreEl.length).toEqual(2); }); diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index 6b5c6578e1f..031a4224692 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -19,6 +19,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-community-list', templateUrl: './community-list.component.html', + styleUrls: ['./community-list.component.scss'], }) export class CommunityListComponent implements OnInit, OnDestroy { diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts index c37f8832f84..5ca544bb541 100644 --- a/src/app/community-page/community-page-routing.module.ts +++ b/src/app/community-page/community-page-routing.module.ts @@ -15,6 +15,10 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { ThemedCommunityPageComponent } from './themed-community-page.component'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; +import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; +import { BrowseByGuard } from '../browse-by/browse-by-guard'; +import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; @NgModule({ imports: [ @@ -48,7 +52,23 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; { path: '', component: ThemedCommunityPageComponent, - pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + component: SubComColSectionComponent, + }, + { + path: 'browse/:id', + pathMatch: 'full', + component: ComcolBrowseByComponent, + canActivate: [BrowseByGuard], + resolve: { + breadcrumb: BrowseByI18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'browse.metadata' }, + }, + ], } ], data: { diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index 671bf28fd1c..b3e577af7d3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -7,7 +7,7 @@ - + @@ -17,7 +17,7 @@ + [title]="'community.page.news'"> @@ -28,10 +28,9 @@ - - + -
+
diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index a5bbff3cee7..206aa54cb07 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -1,16 +1,10 @@ import { mergeMap, filter, map } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; - import { Observable } from 'rxjs'; -import { CommunityDataService } from '../core/data/community-data.service'; import { RemoteData } from '../core/data/remote-data'; import { Bitstream } from '../core/shared/bitstream.model'; - import { Community } from '../core/shared/community.model'; - -import { MetadataService } from '../core/metadata/metadata.service'; - import { fadeInOut } from '../shared/animations/fade'; import { hasValue } from '../shared/empty.util'; import { getAllSucceededRemoteDataPayload} from '../core/shared/operators'; @@ -53,8 +47,6 @@ export class CommunityPageComponent implements OnInit { communityPageRoute$: Observable; constructor( - private communityDataService: CommunityDataService, - private metadata: MetadataService, private route: ActivatedRoute, private router: Router, private authService: AuthService, diff --git a/src/app/community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts index 45ffb2a7868..5ebd7c30572 100644 --- a/src/app/community-page/community-page.module.ts +++ b/src/app/community-page/community-page.module.ts @@ -4,9 +4,9 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { CommunityPageComponent } from './community-page.component'; -import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component'; +import { CommunityPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component'; import { CommunityPageRoutingModule } from './community-page-routing.module'; -import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component'; +import { CommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { StatisticsModule } from '../statistics/statistics.module'; @@ -15,20 +15,25 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component' import { ComcolModule } from '../shared/comcol/comcol.module'; import { ThemedCommunityPageSubCommunityListComponent -} from './sub-community-list/themed-community-page-sub-community-list.component'; +} from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; import { ThemedCollectionPageSubCollectionListComponent -} from './sub-collection-list/themed-community-page-sub-collection-list.component'; +} from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; +import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; +import { BrowseByPageModule } from '../browse-by/browse-by-page.module'; -const DECLARATIONS = [CommunityPageComponent, +const DECLARATIONS = [ + CommunityPageComponent, ThemedCommunityPageComponent, ThemedCommunityPageSubCommunityListComponent, CommunityPageSubCollectionListComponent, ThemedCollectionPageSubCollectionListComponent, CommunityPageSubCommunityListComponent, CreateCommunityPageComponent, - DeleteCommunityPageComponent]; + DeleteCommunityPageComponent, + SubComColSectionComponent, +]; @NgModule({ imports: [ @@ -39,6 +44,7 @@ const DECLARATIONS = [CommunityPageComponent, CommunityFormModule, ComcolModule, DsoPageModule, + BrowseByPageModule, ], declarations: [ ...DECLARATIONS 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 ef5fdade73b..1e71fb7f494 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 @@ -7,11 +7,11 @@

{{ 'community.delete.head' | translat
diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html similarity index 90% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index 69f16ee3ac0..b5fbf1a01dd 100644 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,6 +1,6 @@
-

{{'community.sub-collection-list.head' | translate}}

+

{{'community.sub-collection-list.head' | translate}}

{ let comp: CommunityPageSubCollectionListComponent; diff --git a/src/app/community-page/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 similarity index 79% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index ed14096ce03..92e689b127f 100644 --- a/src/app/community-page/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,19 +1,17 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; - import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; - -import { RemoteData } from '../../core/data/remote-data'; -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 { RemoteData } from '../../../../core/data/remote-data'; +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 { hasValue } from '../../../../shared/empty.util'; @Component({ selector: 'ds-community-page-sub-collection-list', diff --git a/src/app/community-page/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 similarity index 68% rename from src/app/community-page/sub-collection-list/themed-community-page-sub-collection-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index f1f49f204c2..ebbec33e8e9 100644 --- a/src/app/community-page/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,12 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; +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'; +import { Community } from '../../../../core/shared/community.model'; @Component({ selector: 'ds-themed-community-page-sub-collection-list', styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', + templateUrl: '../../../../shared/theme-support/themed.component.html', }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; @@ -18,7 +18,7 @@ export class ThemedCollectionPageSubCollectionListComponent extends ThemedCompon } protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`); + return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component`); } protected importUnthemedComponent(): Promise { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html new file mode 100644 index 00000000000..515e08ffdfe --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss 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 new file mode 100644 index 00000000000..804299d3d98 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -0,0 +1,32 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SubComColSectionComponent } from './sub-com-col-section.component'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; + +describe('SubComColSectionComponent', () => { + let component: SubComColSectionComponent; + let fixture: ComponentFixture; + + let activatedRoute: ActivatedRouteStub; + + beforeEach(async () => { + activatedRoute = new ActivatedRouteStub(); + + await TestBed.configureTestingModule({ + declarations: [ + SubComColSectionComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SubComColSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 00000000000..ff30e51607b --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { ActivatedRoute, Data } from '@angular/router'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'ds-sub-com-col-section', + templateUrl: './sub-com-col-section.component.html', + styleUrls: ['./sub-com-col-section.component.scss'], +}) +export class SubComColSectionComponent implements OnInit { + + community$: Observable; + + constructor( + private route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.community$ = this.route.data.pipe( + map((data: Data) => (data.dso as RemoteData).payload), + ); + } + +} diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html similarity index 90% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index be2788a9f40..0834d08ba58 100644 --- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html @@ -1,6 +1,6 @@
-

{{'community.sub-community-list.head' | translate}}

+

{{'community.sub-community-list.head' | translate}}

{ let comp: CommunityPageSubCommunityListComponent; diff --git a/src/app/community-page/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 similarity index 81% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 08c9509edbd..3108be8a601 100644 --- a/src/app/community-page/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 @@ -3,16 +3,16 @@ import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -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 { RemoteData } from '../../../../core/data/remote-data'; +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 { PaginationService } from '../../../../core/pagination/pagination.service'; +import { hasValue } from '../../../../shared/empty.util'; @Component({ selector: 'ds-community-page-sub-community-list', diff --git a/src/app/community-page/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 similarity index 68% rename from src/app/community-page/sub-community-list/themed-community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 852c53186ef..9c500cac10d 100644 --- a/src/app/community-page/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,12 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; +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'; +import { Community } from '../../../../core/shared/community.model'; @Component({ selector: 'ds-themed-community-page-sub-community-list', styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', + templateUrl: '../../../../shared/theme-support/themed.component.html', }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { @@ -19,7 +19,7 @@ export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponen } protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`); + return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component`); } protected importUnthemedComponent(): Promise { diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts new file mode 100644 index 00000000000..b6f41424693 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -0,0 +1,31 @@ +import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; + +describe('PublicationClaimBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: PublicationClaimBreadcrumbResolver; + let publicationClaimBreadcrumbService: any; + const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; + const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; + const expectedId = 'openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; + let route; + + beforeEach(() => { + route = { + paramMap: { + get: function (param) { + return this[param]; + }, + targetId: expectedId, + } + }; + publicationClaimBreadcrumbService = {}; + resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); + }); + + it('should resolve the breadcrumb config', () => { + const resolvedConfig = resolver.resolve(route as any, {url: fullPath } as any); + 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 new file mode 100644 index 00000000000..713500d6a73 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -0,0 +1,24 @@ +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'; + +@Injectable({ + providedIn: 'root' +}) +export class PublicationClaimBreadcrumbResolver implements Resolve> { + constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { + } + + /** + * 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 }; + } +} diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts new file mode 100644 index 00000000000..11062210bb3 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts @@ -0,0 +1,51 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { getTestScheduler } from 'jasmine-marbles'; +import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { of } from 'rxjs'; + +describe('PublicationClaimBreadcrumbService', () => { + let service: PublicationClaimBreadcrumbService; + let dsoNameService: any = { + getName: (str) => str + }; + let translateService: any = { + instant: (str) => str, + }; + + let dataService: any = { + findById: (str) => createSuccessfulRemoteDataObject$(str), + }; + + let authorizationService: any = { + isAuthorized: (str) => of(true), + }; + + let exampleKey; + + const ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim'; + const ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title'; + + function init() { + exampleKey = 'suggestion.suggestionFor.breadcrumb'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new PublicationClaimBreadcrumbService(dataService,dsoNameService,translateService, authorizationService); + }); + + describe('getBreadcrumbs', () => { + 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)] + }); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts new file mode 100644 index 00000000000..1a87fd7de60 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts @@ -0,0 +1,46 @@ +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 { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../data/feature-authorization/feature-id'; + + + +/** + * Service to calculate Publication claims breadcrumbs + */ +@Injectable({ + providedIn: 'root' +}) +export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderService { + private ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim'; + private ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title'; + + constructor(private dataService: ItemDataService, + private dsoNameService: DSONameService, + private tranlsateService: TranslateService, + protected authorizationService: AuthorizationDataService) { + } + + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + */ + getBreadcrumbs(key: string): Observable { + return combineLatest([this.dataService.findById(key).pipe(getFirstCompletedRemoteData()),this.authorizationService.isAuthorized(FeatureID.AdministratorOf)]).pipe( + 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)]; + }) + ); + } +} diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b210b349494..58bbc0b8700 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -105,7 +105,7 @@ export class BrowseService { }) ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -153,7 +153,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/cache/builders/build-decorators.spec.ts b/src/app/core/cache/builders/build-decorators.spec.ts index 150a07f0066..e4baaa4a5a8 100644 --- a/src/app/core/cache/builders/build-decorators.spec.ts +++ b/src/app/core/cache/builders/build-decorators.spec.ts @@ -1,7 +1,7 @@ import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import { getLinkDefinition, link } from './build-decorators'; +import { dataService, getDataServiceFor, getLinkDefinition, link } from './build-decorators'; class TestHALResource implements HALResource { _links: { @@ -46,5 +46,17 @@ describe('build decorators', () => { expect(result).toBeUndefined(); }); }); + + describe(`set data service`, () => { + it(`should throw error`, () => { + expect(dataService(null)).toThrow(); + }); + + it(`should set properly data service for type`, () => { + const target = new TestHALResource(); + dataService(testType)(target); + expect(getDataServiceFor(testType)).toEqual(target); + }); + }); }); }); diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 9e5ebaed854..8da1861ecf4 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -3,13 +3,20 @@ 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 { + getResourceTypeValueFor +} from '../object-cache.reducer'; import { InjectionToken } from '@angular/core'; +import { CacheableObject } from '../cacheable-object.model'; import { TypedObject } from '../typed-object.model'; +export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>('getDataServiceFor', { + providedIn: 'root', + 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', @@ -20,6 +27,7 @@ const resolvedLinkKey = Symbol('resolvedLink'); const resolvedLinkMap = new Map(); const typeMap = new Map(); +const dataServiceMap = new Map(); const linkMap = new Map(); /** @@ -38,6 +46,39 @@ export function getClassForType(type: string | ResourceType) { return typeMap.get(getResourceTypeValueFor(type)); } +/** + * A class decorator to indicate that this class is a dataservice + * for a given resource type. + * + * "dataservice" in this context means that it has findByHref and + * findAllByHref methods. + * + * @param resourceType the resource type the class is a dataservice for + */ +export function dataService(resourceType: ResourceType): any { + return (target: any) => { + 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) { + return dataServiceMap.get(resourceType.value); +} + /** * A class to represent the data that can be set by the @link decorator */ @@ -65,7 +106,7 @@ export const link = ( resourceType: ResourceType, isList = false, linkName?: keyof T['_links'], - ) => { +) => { return (target: T, propertyName: string) => { let targetMap = linkMap.get(target.constructor); 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 42cff05abe1..cd63ff64366 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -161,6 +161,7 @@ export class RemoteDataBuildService { } else { // in case the elements of the paginated list were already filled in, because they're UnCacheableObjects paginatedList.page = paginatedList.page + .filter((obj: any) => obj != null) .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index b3abf5f877e..f151f10f661 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -185,6 +185,8 @@ 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 { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model'; +import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -386,7 +388,9 @@ export const models = IdentifierData, Subscription, ItemRequest, - BulkAccessConditionOptions + BulkAccessConditionOptions, + SuggestionTarget, + SuggestionSource ]; @NgModule({ diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index 747f172fcd5..35f5e432993 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -30,11 +30,11 @@ export const EMBED_SEPARATOR = '%2F'; /** * Common functionality for data services. * Specific functionality that not all services would need - * is implemented in "DataService feature" classes (e.g. {@link CreateData} + * is implemented in "UpdateDataServiceImpl feature" classes (e.g. {@link CreateData} * - * All DataService (or DataService feature) classes must + * All UpdateDataServiceImpl (or UpdateDataServiceImpl feature) classes must * - extend this class (or {@link IdentifiableDataService}) - * - implement any DataService features it requires in order to forward calls to it + * - implement any UpdateDataServiceImpl features it requires in order to forward calls to it * * ``` * export class SomeDataService extends BaseDataService implements CreateData, SearchData { @@ -422,7 +422,7 @@ export class BaseDataService implements HALDataServic /** * Return the links to traverse from the root of the api to the - * endpoint this DataService represents + * endpoint this UpdateDataServiceImpl represents * * e.g. if the api root links to 'foo', and the endpoint at 'foo' * links to 'bar' the linkPath for the BarDataService would be diff --git a/src/app/core/data/base/create-data.ts b/src/app/core/data/base/create-data.ts index 3ffcd9adf20..d2e009f6698 100644 --- a/src/app/core/data/base/create-data.ts +++ b/src/app/core/data/base/create-data.ts @@ -37,7 +37,7 @@ export interface CreateData { } /** - * A DataService feature to create objects. + * A UpdateDataServiceImpl feature to create objects. * * Concrete data services can use this feature by implementing {@link CreateData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/base/find-all-data.ts b/src/app/core/data/base/find-all-data.ts index 57884e537e5..bc0c1fb613e 100644 --- a/src/app/core/data/base/find-all-data.ts +++ b/src/app/core/data/base/find-all-data.ts @@ -42,7 +42,7 @@ export interface FindAllData { } /** - * A DataService feature to list all objects. + * A UpdateDataServiceImpl feature to list all objects. * * Concrete data services can use this feature by implementing {@link FindAllData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/base/patch-data.ts b/src/app/core/data/base/patch-data.ts index e30c394a347..1f93671458f 100644 --- a/src/app/core/data/base/patch-data.ts +++ b/src/app/core/data/base/patch-data.ts @@ -54,7 +54,7 @@ export interface PatchData { } /** - * A DataService feature to patch and update objects. + * A UpdateDataServiceImpl feature to patch and update objects. * * Concrete data services can use this feature by implementing {@link PatchData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/base/put-data.ts b/src/app/core/data/base/put-data.ts index bd2a8d29299..66ae73405eb 100644 --- a/src/app/core/data/base/put-data.ts +++ b/src/app/core/data/base/put-data.ts @@ -31,7 +31,7 @@ export interface PutData { } /** - * A DataService feature to send PUT requests. + * A UpdateDataServiceImpl feature to send PUT requests. * * Concrete data services can use this feature by implementing {@link PutData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/base/search-data.ts b/src/app/core/data/base/search-data.ts index 536d6d6e254..ff0b4929458 100644 --- a/src/app/core/data/base/search-data.ts +++ b/src/app/core/data/base/search-data.ts @@ -51,7 +51,7 @@ export interface SearchData { } /** - * A DataService feature to search for objects. + * A UpdateDataServiceImpl feature to search for objects. * * Concrete data services can use this feature by implementing {@link SearchData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index e2943f1762f..b5f0c60dc6b 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -34,5 +34,6 @@ export enum FeatureID { CanEditItem = 'canEditItem', CanRegisterDOI = 'canRegisterDOI', CanSubscribe = 'canSubscribeDso', - CanSeeQA = 'canSeeQA' + EPersonForgotPassword = 'epersonForgotPassword', + CanSeeQA = 'canSeeQA', } diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index 0a765de101b..8393d714605 100644 --- a/src/app/core/data/href-only-data.service.ts +++ b/src/app/core/data/href-only-data.service.ts @@ -17,8 +17,8 @@ import { HALDataService } from './base/hal-data-service.interface'; import { dataService } from './base/data-service.decorator'; /** - * A DataService with only findByHref methods. Its purpose is to be used for resources that don't - * need to be retrieved by ID, or have any way to update them, but require a DataService in order + * A UpdateDataServiceImpl with only findByHref methods. Its purpose is to be used for resources that don't + * need to be retrieved by ID, or have any way to update them, but require a UpdateDataServiceImpl in order * for their links to be resolved by the LinkService. * * an @dataService annotation can be added for any number of these resource types diff --git a/src/app/core/data/update-data.service.spec.ts b/src/app/core/data/update-data.service.spec.ts new file mode 100644 index 00000000000..426fa87eb6d --- /dev/null +++ b/src/app/core/data/update-data.service.spec.ts @@ -0,0 +1,144 @@ +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +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 { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { HrefOnlyDataService } from './href-only-data.service'; +import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; +import { RestResponse } from '../cache/response.models'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { Item } from '../shared/item.model'; +import { Version } from '../shared/version.model'; +import { VersionHistory } from '../shared/version-history.model'; +import { RequestEntry } from './request-entry.model'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { UpdateDataServiceImpl } from './update-data.service'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testCreateDataImplementation } from './base/create-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { testPutDataImplementation } from './base/put-data.spec'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; + +describe('VersionDataService test', () => { + let scheduler: TestScheduler; + let service: UpdateDataServiceImpl; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let hrefOnlyDataService: HrefOnlyDataService; + let responseCacheEntry: RequestEntry; + + const notificationsService = {} as NotificationsService; + + const item = Object.assign(new Item(), { + id: '1234-1234', + uuid: '1234-1234', + bundles: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.type': [ + { + language: null, + value: 'Article' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ] + } + }); + + const versionHistory = Object.assign(new VersionHistory(), { + id: '1', + draftVersion: true, + }); + + const mockVersion: Version = Object.assign(new Version(), { + item: createSuccessfulRemoteDataObject$(item), + versionhistory: createSuccessfulRemoteDataObject$(versionHistory), + version: 1, + }); + const mockVersionRD = createSuccessfulRemoteDataObject(mockVersion); + + const endpointURL = `https://rest.api/rest/api/versioning/versions`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + objectCache = {} as ObjectCacheService; + const comparatorEntry = {} as any; + function initTestService() { + hrefOnlyDataService = getMockHrefOnlyDataService(); + return new UpdateDataServiceImpl( + 'testLinkPath', + requestService, + rdbService, + objectCache, + halService, + notificationsService, + comparatorEntry, + 10 * 1000 + ); + } + + beforeEach(() => { + + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + 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: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('(a|)', { + a: mockVersionRD + }) + }); + + service = initTestService(); + + spyOn((service as any), 'findById').and.callThrough(); + }); + + afterEach(() => { + service = null; + }); + + describe('composition', () => { + const initService = () => new UpdateDataServiceImpl(null, null, null, null, null, null, null, null); + + testPatchDataImplementation(initService); + testSearchDataImplementation(initService); + testDeleteDataImplementation(initService); + testCreateDataImplementation(initService); + testFindAllDataImplementation(initService); + testPutDataImplementation(initService); + }); + +}); diff --git a/src/app/core/data/update-data.service.ts b/src/app/core/data/update-data.service.ts index 9f707a82da9..715b2ee413c 100644 --- a/src/app/core/data/update-data.service.ts +++ b/src/app/core/data/update-data.service.ts @@ -1,13 +1,317 @@ -import { Observable } from 'rxjs'; +import { Operation } from 'fast-json-patch'; +import { AsyncSubject, from as observableFrom, Observable } from 'rxjs'; +import { + find, + map, + mergeMap, + switchMap, + take, + toArray +} 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 { 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 { ChangeAnalyzer } from './change-analyzer'; +import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; +import { + DeleteByIDRequest, + PostRequest +} from './request.models'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; -import { Operation } from 'fast-json-patch'; +import { NoContent } from '../shared/NoContent.model'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { FindListOptions } from './find-list-options.model'; +import { FindAllData, FindAllDataImpl } from './base/find-all-data'; +import { SearchData, SearchDataImpl } from './base/search-data'; +import { CreateData, CreateDataImpl } from './base/create-data'; +import { PatchData, PatchDataImpl } from './base/patch-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { PutData, PutDataImpl } from './base/put-data'; +import { DeleteData, DeleteDataImpl } from './base/delete-data'; + /** - * Represents a data service to update a given object + * Interface to list the methods used by the injected service in components */ export interface UpdateDataService { patch(dso: T, operations: Operation[]): Observable>; update(object: T): Observable>; - commitUpdates(method?: RestRequestMethod); + commitUpdates(method?: RestRequestMethod): void; +} + + +/** + * Specific functionalities that not all services would need. + * Goal of the class is to update remote objects, handling custom methods that don't belong to BaseDataService + * The class implements also the following common interfaces + * + * findAllData: FindAllData; + * searchData: SearchData; + * createData: CreateData; + * patchData: PatchData; + * putData: PutData; + * deleteData: DeleteData; + * + * Custom methods are: + * + * deleteOnRelated - delete all related objects to the given one + * postOnRelated - post all the related objects to the given one + * invalidate - invalidate the DSpaceObject making all requests as stale + * invalidateByHref - invalidate the href making all requests as stale + */ + +export class UpdateDataServiceImpl extends IdentifiableDataService implements FindAllData, SearchData, CreateData, PatchData, PutData, DeleteData { + private findAllData: FindAllDataImpl; + private searchData: SearchDataImpl; + private createData: CreateDataImpl; + private patchData: PatchDataImpl; + private putData: PutDataImpl; + private deleteData: DeleteDataImpl; + + + constructor( + protected linkPath: string, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected comparator: ChangeAnalyzer, + protected responseMsToLive: number, + ) { + super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService ,this.responseMsToLive); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator ,this.responseMsToLive, this.constructIdEndpoint); + this.putData = new PutDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService ,this.responseMsToLive, this.constructIdEndpoint); + } + + + /** + * Create the HREF with given options object + * + * @param options The [[FindListOptions]] object + * @param linkPath The link path for the object + * @return {Observable} + * Return an observable that emits created HREF + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig[]): Observable { + return this.findAllData.getFindAllHref(options, linkPath, ...linksToFollow); + } + + /** + * Create the HREF for a specific object's search method with given options object + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @return {Observable} + * Return an observable that emits created HREF + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable { + return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow); + } + + /** + * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded + * info should be added to the objects + * + * @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>>} + * Return an observable that emits object list + */ + findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Make a new FindListRequest with given search method + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] 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>} + * Return an observable that emits response from the server + */ + searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Send a patch request for a specified object + * @param {T} object The object to send a patch request for + * @param {Operation[]} operations The patch operations to be performed + */ + patch(object: T, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + createPatchFromCache(object: T): Observable { + return this.patchData.createPatchFromCache(object); + } + + /** + * Send a PUT request for the specified object + * + * @param object The object to send a put request for. + */ + put(object: T): Observable> { + return this.putData.put(object); + } + + /** + * Add a new patch to the object cache + * The patch is derived from the differences between the given object and its version in the object cache + * @param {DSpaceObject} object The given object + */ + update(object: T): Observable> { + return this.patchData.update(object); + } + + /** + * Create a new DSpaceObject on the server, and store the response + * in the object cache + * + * @param {CacheableObject} object + * The object to create + * @param {RequestParam[]} params + * Array with additional params to combine with query string + */ + create(object: T, ...params: RequestParam[]): Observable> { + return this.createData.create(object, ...params); + } + + /** + <<<<<<< HEAD + * Perform a post on an endpoint related item with ID. Ex.: endpoint//related?item= + * @param itemId The item id + * @param relatedItemId The related item Id + * @param body The optional POST body + * @return the RestResponse as an Observable + */ + public postOnRelated(itemId: string, relatedItemId: string, body?: any) { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getIDHrefObs(itemId); + + 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); + } + + /** + * Perform a delete on an endpoint related item. Ex.: endpoint//related + * @param itemId The item id + * @return the RestResponse as an Observable + */ + public deleteOnRelated(itemId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getIDHrefObs(itemId); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href + '/related', itemId); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + /* + * Invalidate an existing DSpaceObject by marking all requests it is included in as stale + * @param objectId The id of the object to be invalidated + * @return An Observable that will emit `true` once all requests are stale + */ + invalidate(objectId: string): Observable { + return this.getIDHrefObs(objectId).pipe( + switchMap((href: string) => this.invalidateByHref(href)) + ); + } + + /** + * Invalidate an existing DSpaceObject by marking all requests it is included in as stale + * @param href The self link of the object to be invalidated + * @return An Observable that will emit `true` once all requests are stale + */ + invalidateByHref(href: string): Observable { + const done$ = new AsyncSubject(); + + this.objectCache.getByHref(href).pipe( + switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe( + mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), + toArray(), + )), + ).subscribe(() => { + done$.next(true); + done$.complete(); + }); + + return done$; + } + + /** + * Delete an existing DSpace Object on the server + * @param objectId The id of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc + */ + delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } + + /** + * Delete an existing DSpace Object on the server + * @param href The self link of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc + * Only emits once all request related to the DSO has been invalidated. + */ + deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + /** + * Commit current object changes to the server + * @param method The RestRequestMethod for which de server sync buffer should be committed + */ + commitUpdates(method?: RestRequestMethod) { + this.patchData.commitUpdates(method); + } } diff --git a/src/app/core/data/version-data.service.spec.ts b/src/app/core/data/version-data.service.spec.ts index dd3f9eec945..0579b998b87 100644 --- a/src/app/core/data/version-data.service.spec.ts +++ b/src/app/core/data/version-data.service.spec.ts @@ -128,7 +128,7 @@ describe('VersionDataService test', () => { }); describe('getHistoryFromVersion', () => { - it('should proxy the call to DataService.findByHref', () => { + it('should proxy the call to UpdateDataServiceImpl.findByHref', () => { scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true)); scheduler.flush(); diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index c1bc3563a32..cbddf1e6c3e 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -315,7 +315,7 @@ describe('EPersonDataService', () => { service.deleteEPerson(EPersonMock).subscribe(); }); - it('should call DataService.delete with the EPerson\'s UUID', () => { + it('should call UpdateDataServiceImpl.delete with the EPerson\'s UUID', () => { expect(service.delete).toHaveBeenCalledWith(EPersonMock.id); }); }); diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts index 5e00027edb9..86edd339e8f 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.ts @@ -351,7 +351,28 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath })); break; } - return newBody; + return dedupeOperationEntries(newBody); +} + +/** + * Dedupe operation entries by op and path. This prevents processing unnecessary patches in a single PATCH request. + * + * @param body JSON patch operation object entries + * @returns deduped JSON patch operation object entries + */ +function dedupeOperationEntries(body: JsonPatchOperationObject[]): JsonPatchOperationObject[] { + const ops = new Map(); + for (let i = body.length - 1; i >= 0; i--) { + const patch = body[i].operation; + const key = `${patch.op}-${patch.path}`; + if (!ops.has(key)) { + ops.set(key, patch); + } else { + body.splice(i, 1); + } + } + + return body; } function makeOperationEntry(operation) { diff --git a/src/app/core/rest-property/forgot-password-check-guard.guard.ts b/src/app/core/rest-property/forgot-password-check-guard.guard.ts new file mode 100644 index 00000000000..438a532c7b9 --- /dev/null +++ b/src/app/core/rest-property/forgot-password-check-guard.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../data/feature-authorization/feature-id'; +import { + SingleFeatureAuthorizationGuard +} from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard'; +import { AuthService } from '../auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard that checks if the forgot-password feature is enabled + */ +export class ForgotPasswordCheckGuard extends SingleFeatureAuthorizationGuard { + + constructor( + protected readonly authorizationService: AuthorizationDataService, + protected readonly router: Router, + protected readonly authService: AuthService + ) { + super(authorizationService, router, authService); + } + + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return of(FeatureID.EPersonForgotPassword); + } + +} diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index a5bed53c9fd..72239b26f75 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,5 +1,6 @@ import { autoserialize } from 'cerialize'; import { CacheableObject } from '../cache/cacheable-object.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * Base class for BrowseDefinition models @@ -12,5 +13,5 @@ export abstract class BrowseDefinition extends CacheableObject { /** * Get the render type of the BrowseDefinition model */ - abstract getRenderType(): string; + abstract getRenderType(): BrowseByDataType; } diff --git a/src/app/core/shared/flat-browse-definition.model.ts b/src/app/core/shared/flat-browse-definition.model.ts index 086fca891bb..9f37f1c422c 100644 --- a/src/app/core/shared/flat-browse-definition.model.ts +++ b/src/app/core/shared/flat-browse-definition.model.ts @@ -5,6 +5,7 @@ import { FLAT_BROWSE_DEFINITION } from './flat-browse-definition.resource-type'; import { ResourceType } from './resource-type'; import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition'; import { HALLink } from './hal-link.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'flatBrowse' @@ -30,7 +31,7 @@ export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition { items: HALLink; }; - getRenderType(): string { + getRenderType(): BrowseByDataType { return this.dataType; } } diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts index d561fff643f..2410bf7b7a4 100644 --- a/src/app/core/shared/hierarchical-browse-definition.model.ts +++ b/src/app/core/shared/hierarchical-browse-definition.model.ts @@ -5,6 +5,7 @@ import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { BrowseDefinition } from './browse-definition.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'hierarchicalBrowse' @@ -39,7 +40,7 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition { vocabulary: HALLink; }; - getRenderType(): string { - return 'hierarchy'; + getRenderType(): BrowseByDataType { + return BrowseByDataType.Hierarchy; } } diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts index d5481fcc8d0..e3319affdb9 100644 --- a/src/app/core/shared/non-hierarchical-browse-definition.ts +++ b/src/app/core/shared/non-hierarchical-browse-definition.ts @@ -1,6 +1,6 @@ import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { SortOption } from './sort-option.model'; -import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; import { BrowseDefinition } from './browse-definition.model'; /** diff --git a/src/app/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index 80ba200d383..bf232cb141b 100644 --- a/src/app/core/shared/search/search-filter.service.ts +++ b/src/app/core/shared/search/search-filter.service.ts @@ -27,6 +27,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; export const FILTER_CONFIG: InjectionToken = new InjectionToken('filterConfig'); export const IN_PLACE_SEARCH: InjectionToken = new InjectionToken('inPlaceSearch'); export const REFRESH_FILTER: InjectionToken> = new InjectionToken('refreshFilters'); +export const SCOPE: InjectionToken = new InjectionToken('scope'); /** * Service that performs all actions that have to do with search filters and facets diff --git a/src/app/core/shared/value-list-browse-definition.model.ts b/src/app/core/shared/value-list-browse-definition.model.ts index 3378263962e..0302ec59c73 100644 --- a/src/app/core/shared/value-list-browse-definition.model.ts +++ b/src/app/core/shared/value-list-browse-definition.model.ts @@ -5,6 +5,7 @@ import { VALUE_LIST_BROWSE_DEFINITION } from './value-list-browse-definition.res import { ResourceType } from './resource-type'; import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition'; import { HALLink } from './hal-link.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'valueList' @@ -30,7 +31,7 @@ export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition { entries: HALLink; }; - getRenderType(): string { + getRenderType(): BrowseByDataType { return this.dataType; } } diff --git a/src/app/core/submission/workflowitem-data.service.spec.ts b/src/app/core/submission/workflowitem-data.service.spec.ts index 3f6ec54fdad..64ffbe57180 100644 --- a/src/app/core/submission/workflowitem-data.service.spec.ts +++ b/src/app/core/submission/workflowitem-data.service.spec.ts @@ -126,7 +126,7 @@ describe('WorkflowItemDataService test', () => { }); describe('findByItem', () => { - it('should proxy the call to DataService.findByHref', () => { + it('should proxy the call to UpdateDataServiceImpl.findByHref', () => { scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo)); scheduler.flush(); diff --git a/src/app/core/submission/workspaceitem-data.service.spec.ts b/src/app/core/submission/workspaceitem-data.service.spec.ts index e766a6a039c..25a849baa28 100644 --- a/src/app/core/submission/workspaceitem-data.service.spec.ts +++ b/src/app/core/submission/workspaceitem-data.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; @@ -8,7 +8,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { PageInfo } from '../shared/page-info.model'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { HrefOnlyDataService } from '../data/href-only-data.service'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; import { WorkspaceitemDataService } from './workspaceitem-data.service'; @@ -21,6 +21,11 @@ import { RequestEntry } from '../data/request-entry.model'; import { CoreState } from '../core-state.model'; import { testSearchDataImplementation } from '../data/base/search-data.spec'; import { testDeleteDataImplementation } from '../data/base/delete-data.spec'; +import { SearchData } from '../data/base/search-data'; +import { DeleteData } from '../data/base/delete-data'; +import { RequestParam } from '../cache/models/request-param.model'; +import { PostRequest } from '../data/request.models'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; describe('WorkspaceitemDataService test', () => { let scheduler: TestScheduler; @@ -68,15 +73,12 @@ describe('WorkspaceitemDataService test', () => { const wsiRD = createSuccessfulRemoteDataObject(wsi); const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`; - const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`; - const searchRequestURL$ = observableOf(searchRequestURL); const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; objectCache = {} as ObjectCacheService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; - const comparator = {} as any; const comparatorEntry = {} as any; const store = {} as Store; const pageInfo = new PageInfo(); @@ -84,18 +86,23 @@ describe('WorkspaceitemDataService test', () => { function initTestService() { hrefOnlyDataService = getMockHrefOnlyDataService(); return new WorkspaceitemDataService( + comparatorEntry, + halService, + http, + notificationsService, requestService, rdbService, objectCache, - halService, - notificationsService, + store ); } describe('composition', () => { - const initService = () => new WorkspaceitemDataService(null, null, null, null, null); - testSearchDataImplementation(initService); - testDeleteDataImplementation(initService); + const initSearchService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as SearchData; + const initDeleteService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as DeleteData; + + testSearchDataImplementation(initSearchService); + testDeleteDataImplementation(initDeleteService); }); describe('', () => { @@ -104,7 +111,7 @@ describe('WorkspaceitemDataService test', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: endpointURL }) + getEndpoint: observableOf(endpointURL) }); responseCacheEntry = new RequestEntry(); responseCacheEntry.request = { href: 'https://rest.api/' } as any; @@ -120,13 +127,13 @@ describe('WorkspaceitemDataService test', () => { rdbService = jasmine.createSpyObj('rdbService', { buildSingle: hot('a|', { a: wsiRD - }) + }), + buildFromRequestUUID: createSuccessfulRemoteDataObject$({}) }); service = initTestService(); spyOn((service as any), 'findByHref').and.callThrough(); - spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$); }); afterEach(() => { @@ -134,11 +141,11 @@ describe('WorkspaceitemDataService test', () => { }); describe('findByItem', () => { - it('should proxy the call to DataService.findByHref', () => { + it('should proxy the call to UpdateDataServiceImpl.findByHref', () => { scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo)); scheduler.flush(); - - expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true); + const searchUrl = service.getIDHref('item', [new RequestParam('uuid', encodeURIComponent('1234-1234'))]); + expect((service as any).findByHref).toHaveBeenCalledWith(searchUrl, true, true); }); it('should return a RemoteData for the search', () => { @@ -150,6 +157,19 @@ describe('WorkspaceitemDataService test', () => { }); }); - }); + describe('importExternalSourceEntry', () => { + it('should send a POST request containing the provided item request', (done) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + service.importExternalSourceEntry('externalHref', 'testId').subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestUUID, `${endpointURL}?owningCollection=testId`, 'externalHref', options)); + done(); + }); + }); + }); + }); }); diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index f285fb6eca5..181530a29b3 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -1,46 +1,58 @@ import { Injectable } from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; + +import { Store } from '@ngrx/store'; +import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { WorkspaceItem } from './models/workspaceitem.model'; import { Observable } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RequestParam } from '../cache/models/request-param.model'; +import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; +import {HttpOptions} from '../dspace-rest/dspace-rest.service'; +import {find, map} from 'rxjs/operators'; +import {PostRequest} from '../data/request.models'; +import {hasValue} from '../../shared/empty.util'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { NoContent } from '../shared/NoContent.model'; +import { DeleteData, DeleteDataImpl } from '../data/base/delete-data'; import { SearchData, SearchDataImpl } from '../data/base/search-data'; import { PaginatedList } from '../data/paginated-list.model'; -import { DeleteData, DeleteDataImpl } from '../data/base/delete-data'; -import { NoContent } from '../shared/NoContent.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() @dataService(WorkspaceItem.type) -export class WorkspaceitemDataService extends IdentifiableDataService implements SearchData, DeleteData { +export class WorkspaceitemDataService extends IdentifiableDataService implements DeleteData, SearchData{ + protected linkPath = 'workspaceitems'; protected searchByItemLinkPath = 'item'; - - private searchData: SearchDataImpl; - private deleteData: DeleteDataImpl; + private deleteData: DeleteData; + private searchData: SearchData; constructor( + protected comparator: DSOChangeAnalyzer, + protected halService: HALEndpointService, + protected http: HttpClient, + protected notificationsService: NotificationsService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - ) { + protected store: Store) { super('workspaceitems', requestService, rdbService, objectCache, halService); - - this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); } - /** * Return the WorkspaceItem object found through the UUID of an item * @@ -55,21 +67,46 @@ export class WorkspaceitemDataService extends IdentifiableDataService[]): Observable> { const findListOptions = new FindListOptions(); findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))]; - const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow); + const href$ = this.getIDHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow); return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** - * Create the HREF for a specific object's search method with given options object - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @return {Observable} - * Return an observable that emits created HREF - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * Import an external source entry into a collection + * @param externalSourceEntryHref + * @param collectionId */ - public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow): Observable { - return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow); + public importExternalSourceEntry(externalSourceEntryHref: string, collectionId: string): Observable> { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`)); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, externalSourceEntryHref, options); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + /** + * Delete an existing object on the server + * @param href The self link of the object to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, + * errorMessage, timeCompleted, etc + * Only emits once all request related to the DSO has been invalidated. + */ + deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); } /** @@ -86,33 +123,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService>} * Return an observable that emits response from the server */ - public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } - - /** - * Delete an existing object on the server - * @param objectId The id of the object to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, - * errorMessage, timeCompleted, etc - */ - public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { - return this.deleteData.delete(objectId, copyVirtualMetadata); - } - - /** - * Delete an existing object on the server - * @param href The self link of the object to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, - * errorMessage, timeCompleted, etc - * Only emits once all request related to the DSO has been invalidated. - */ - public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { - return this.deleteData.deleteByHref(href, copyVirtualMetadata); - } - } diff --git a/src/app/core/suggestion-notifications/models/suggestion-objects.resource-type.ts b/src/app/core/suggestion-notifications/models/suggestion-objects.resource-type.ts new file mode 100644 index 00000000000..8f83d86376b --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion-objects.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for the Suggestion object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION = new ResourceType('suggestion'); diff --git a/src/app/core/suggestion-notifications/models/suggestion-source-object.resource-type.ts b/src/app/core/suggestion-notifications/models/suggestion-source-object.resource-type.ts new file mode 100644 index 00000000000..e319ed5109b --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion-source-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for the Suggestion Source object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_SOURCE = new ResourceType('suggestionsource'); diff --git a/src/app/core/suggestion-notifications/models/suggestion-source.model.ts b/src/app/core/suggestion-notifications/models/suggestion-source.model.ts new file mode 100644 index 00000000000..12d9d7e9d84 --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion-source.model.ts @@ -0,0 +1,47 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { SUGGESTION_SOURCE } from './suggestion-source-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 Suggestion Source model + */ +@typedObject +export class SuggestionSource implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION_SOURCE; + + /** + * The Suggestion Target id + */ + @autoserialize + id: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + total: 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, + suggestiontargets: HALLink + }; +} diff --git a/src/app/core/suggestion-notifications/models/suggestion-target-object.resource-type.ts b/src/app/core/suggestion-notifications/models/suggestion-target-object.resource-type.ts new file mode 100644 index 00000000000..81b1b5c2619 --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion-target-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for the Suggestion Target object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_TARGET = new ResourceType('suggestiontarget'); diff --git a/src/app/core/suggestion-notifications/models/suggestion-target.model.ts b/src/app/core/suggestion-notifications/models/suggestion-target.model.ts new file mode 100644 index 00000000000..99d9a8628ad --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion-target.model.ts @@ -0,0 +1,61 @@ +import { autoserialize, deserialize } from 'cerialize'; + + +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { SUGGESTION_TARGET } from './suggestion-target-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'; + +/** + * The interface representing the Suggestion Target model + */ +@typedObject +export class SuggestionTarget implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION_TARGET; + + /** + * The Suggestion Target id + */ + @autoserialize + id: string; + + /** + * The Suggestion Target name to display + */ + @autoserialize + display: string; + + /** + * The Suggestion Target source to display + */ + @autoserialize + source: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + total: 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, + suggestions: HALLink, + target: HALLink + }; +} diff --git a/src/app/core/suggestion-notifications/models/suggestion.model.ts b/src/app/core/suggestion-notifications/models/suggestion.model.ts new file mode 100644 index 00000000000..ad58b1cfe55 --- /dev/null +++ b/src/app/core/suggestion-notifications/models/suggestion.model.ts @@ -0,0 +1,88 @@ +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; + +import { SUGGESTION } from './suggestion-objects.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 { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; +import {CacheableObject} from '../../cache/cacheable-object.model'; + +/** + * The interface representing Suggestion Evidences such as scores (authorScore, datescore) + */ +export interface SuggestionEvidences { + [sectionId: string]: { + score: string; + notes: string + }; +} +/** + * The interface representing the Suggestion Source model + */ +@typedObject +export class Suggestion implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION; + + /** + * The Suggestion id + */ + @autoserialize + id: string; + + /** + * The Suggestion name to display + */ + @autoserialize + display: string; + + /** + * The Suggestion source to display + */ + @autoserialize + source: string; + + /** + * The Suggestion external source uri + */ + @autoserialize + externalSourceUri: string; + + /** + * The Total Score of the suggestion + */ + @autoserialize + score: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + evidences: SuggestionEvidences; + + /** + * All metadata of this suggestion object + */ + @excludeFromEquals + @autoserializeAs(MetadataMapSerializer) + metadata: MetadataMap; + + /** + * 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 + }; +} diff --git a/src/app/core/suggestion-notifications/source/suggestion-source-data.service.ts b/src/app/core/suggestion-notifications/source/suggestion-source-data.service.ts new file mode 100644 index 00000000000..f00a84c95b5 --- /dev/null +++ b/src/app/core/suggestion-notifications/source/suggestion-source-data.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../../data/base/data-service.decorator'; +import { SUGGESTION_SOURCE } from '../models/suggestion-source-object.resource-type'; +import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +import { SuggestionSource } from '../models/suggestion-source.model'; +import { FindAllData, FindAllDataImpl } from '../../data/base/find-all-data'; +import { Store } from '@ngrx/store'; +import { RequestService } from '../../data/request.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CoreState } from '../../core-state.model'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { FindListOptions } from '../../data/find-list-options.model'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../data/paginated-list.model'; +import { RemoteData } from '../../data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; + +/** + * Service that retrieves Suggestion Source data + */ +@Injectable() +@dataService(SUGGESTION_SOURCE) +export class SuggestionSourceDataService extends IdentifiableDataService { + + protected linkPath = 'suggestionsources'; + + private findAllData: FindAllData; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super('suggestionsources', requestService, rdbService, objectCache, halService); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + /** + * Return the list of Suggestion 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.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Return a single Suggestoin 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); + } + + + /** + * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded + * info should be added to the objects + * + * @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>>} + * Return an observable that emits object list + */ + findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } +} diff --git a/src/app/core/suggestion-notifications/source/suggestions-source-data.service.spec.ts b/src/app/core/suggestion-notifications/source/suggestions-source-data.service.spec.ts new file mode 100644 index 00000000000..28f34b863d8 --- /dev/null +++ b/src/app/core/suggestion-notifications/source/suggestions-source-data.service.spec.ts @@ -0,0 +1,115 @@ +import { TestScheduler } from 'rxjs/testing'; +import { RequestService } from '../../data/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 { RequestEntry } from '../../data/request-entry.model'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { RestResponse } from '../../cache/response.models'; +import { of as observableOf } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../../core-state.model'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; +import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec'; +import { FindAllData } from '../../data/base/find-all-data'; +import { GetRequest } from '../../data/request.models'; +import { + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { RemoteData } from '../../data/remote-data'; +import { RequestEntryState } from '../../data/request-entry-state.model'; +import { SuggestionSourceDataService } from './suggestion-source-data.service'; +import { SuggestionSource } from '../models/suggestion-source.model'; + +describe('SuggestionSourceDataService test', () => { + let scheduler: TestScheduler; + let service: SuggestionSourceDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparator: DefaultChangeAnalyzer; + let responseCacheEntry: RequestEntry; + + const store = {} as Store; + const endpointURL = `https://rest.api/rest/api/suggestionsources`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new SuggestionSourceDataService( + requestService, + rdbService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + http = {} as HttpClient; + notificationsService = {} as NotificationsService; + comparator = {} as DefaultChangeAnalyzer; + 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: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(endpointURL) + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }) + }); + + + service = initTestService(); + }); + + describe('composition', () => { + const initFindAllService = () => new SuggestionSourceDataService(null, null, null, null, null, null, null, null) as unknown as FindAllData; + testFindAllDataImplementation(initFindAllService); + }); + + describe('getSources', () => { + it('should send a new GetRequest', () => { + const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}`); + scheduler.schedule(() => service.getSources().subscribe()); + scheduler.flush(); + + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + }); + + describe('getSource', () => { + it('should send a new GetRequest', () => { + const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/testId`); + scheduler.schedule(() => service.getSource('testId').subscribe()); + scheduler.flush(); + + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + }); +}); diff --git a/src/app/core/suggestion-notifications/suggestion-data.service.spec.ts b/src/app/core/suggestion-notifications/suggestion-data.service.spec.ts new file mode 100644 index 00000000000..c0bc97ea129 --- /dev/null +++ b/src/app/core/suggestion-notifications/suggestion-data.service.spec.ts @@ -0,0 +1,173 @@ +import { TestScheduler } from 'rxjs/testing'; +import { SuggestionDataServiceImpl, SuggestionsDataService } from './suggestions-data.service'; +import { RequestService } from '../data/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 { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { Suggestion } from './models/suggestion.model'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { RequestEntry } from '../data/request-entry.model'; +import { RestResponse } from '../cache/response.models'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RemoteData } from '../data/remote-data'; +import { RequestEntryState } from '../data/request-entry-state.model'; +import { SuggestionSource } from './models/suggestion-source.model'; +import { SuggestionTarget } from './models/suggestion-target.model'; +import { SuggestionSourceDataService } from './source/suggestion-source-data.service'; +import { SuggestionTargetDataService } from './target/suggestion-target-data.service'; +import { RequestParam } from '../cache/models/request-param.model'; + +describe('SuggestionDataService test', () => { + let scheduler: TestScheduler; + let service: SuggestionsDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparatorSuggestion: DefaultChangeAnalyzer; + let comparatorSuggestionSource: DefaultChangeAnalyzer; + let comparatorSuggestionTarget: DefaultChangeAnalyzer; + let suggestionSourcesDataService: SuggestionSourceDataService; + let suggestionTargetsDataService: SuggestionTargetDataService; + let suggestionsDataService: SuggestionDataServiceImpl; + let responseCacheEntry: RequestEntry; + + + const testSource = 'test-source'; + const testUserId = '1234-4321'; + const endpointURL = `https://rest.api/rest/api/`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new SuggestionsDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + comparatorSuggestion, + comparatorSuggestionSource, + comparatorSuggestionTarget + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + http = {} as HttpClient; + notificationsService = {} as NotificationsService; + comparatorSuggestion = {} as DefaultChangeAnalyzer; + comparatorSuggestionTarget = {} as DefaultChangeAnalyzer; + comparatorSuggestionSource = {} as DefaultChangeAnalyzer; + 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: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + setStaleByHrefSubstring: observableOf(true) + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(endpointURL) + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }) + }); + + + suggestionSourcesDataService = jasmine.createSpyObj('suggestionSourcesDataService', { + getSources: observableOf(null), + }); + + suggestionTargetsDataService = jasmine.createSpyObj('suggestionTargetsDataService', { + getTargets: observableOf(null), + getTargetsByUser: observableOf(null), + findById: observableOf(null), + }); + + suggestionsDataService = jasmine.createSpyObj('suggestionsDataService', { + searchBy: observableOf(null), + delete: observableOf(null), + }); + + + service = initTestService(); + /* eslint-disable-next-line @typescript-eslint/dot-notation */ + service['suggestionSourcesDataService'] = suggestionSourcesDataService; + /* eslint-disable-next-line @typescript-eslint/dot-notation */ + service['suggestionTargetsDataService'] = suggestionTargetsDataService; + /* eslint-disable-next-line @typescript-eslint/dot-notation */ + service['suggestionsDataService'] = suggestionsDataService; + }); + + describe('Suggestion targets service', () => { + it('should call suggestionSourcesDataService.getTargets', () => { + const options = { + searchParams: [new RequestParam('source', testSource)] + }; + service.getTargets(testSource); + expect(suggestionTargetsDataService.getTargets).toHaveBeenCalledWith('findBySource', options); + }); + + it('should call suggestionSourcesDataService.getTargetsByUser', () => { + const options = { + searchParams: [new RequestParam('target', testUserId)] + }; + service.getTargetsByUser(testUserId); + expect(suggestionTargetsDataService.getTargetsByUser).toHaveBeenCalledWith(testUserId, options); + }); + + it('should call suggestionSourcesDataService.getTargetById', () => { + service.getTargetById('1'); + expect(suggestionTargetsDataService.findById).toHaveBeenCalledWith('1'); + }); + }); + + + describe('Suggestion sources service', () => { + it('should call suggestionSourcesDataService.getSources', () => { + service.getSources(); + expect(suggestionSourcesDataService.getSources).toHaveBeenCalled(); + }); + }); + + describe('Suggestion service', () => { + it('should call suggestionsDataService.searchBy', () => { + const options = { + searchParams: [new RequestParam('target', testUserId), new RequestParam('source', testSource)] + }; + service.getSuggestionsByTargetAndSource(testUserId, testSource); + expect(suggestionsDataService.searchBy).toHaveBeenCalledWith('findByTargetAndSource', options, false, true); + }); + + it('should call suggestionsDataService.delete', () => { + service.deleteSuggestion('1'); + expect(suggestionsDataService.delete).toHaveBeenCalledWith('1'); + }); + }); + + describe('Request service', () => { + it('should call requestService.setStaleByHrefSubstring', () => { + service.clearSuggestionRequests(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/core/suggestion-notifications/suggestions-data.service.ts b/src/app/core/suggestion-notifications/suggestions-data.service.ts new file mode 100644 index 00000000000..17b14825782 --- /dev/null +++ b/src/app/core/suggestion-notifications/suggestions-data.service.ts @@ -0,0 +1,229 @@ +/* eslint-disable max-classes-per-file */ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Store } from '@ngrx/store'; + +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 '../cache/builders/build-decorators'; +import { RequestService } from '../data/request.service'; +import { UpdateDataServiceImpl } from '../data/update-data.service'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { RemoteData } from '../data/remote-data'; +import { SUGGESTION } from './models/suggestion-objects.resource-type'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../data/paginated-list.model'; +import { SuggestionSource } from './models/suggestion-source.model'; +import { SuggestionTarget } from './models/suggestion-target.model'; +import { Suggestion } from './models/suggestion.model'; +import { RequestParam } from '../cache/models/request-param.model'; +import { NoContent } from '../shared/NoContent.model'; +import {CoreState} from '../core-state.model'; +import {FindListOptions} from '../data/find-list-options.model'; +import { SuggestionSourceDataService } from './source/suggestion-source-data.service'; +import { SuggestionTargetDataService } from './target/suggestion-target-data.service'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private UpdateDataServiceImpl implementation to delegate specific methods to. + */ +export class SuggestionDataServiceImpl extends UpdateDataServiceImpl { + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {ChangeAnalyzer} comparator + * @param responseMsToLive + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer, + protected responseMsToLive: number, + ) { + super('suggestions', requestService, rdbService, objectCache, halService, notificationsService, comparator ,responseMsToLive); + } +} + +/** + * The service handling all Suggestion Target REST requests. + */ +@Injectable() +@dataService(SUGGESTION) +export class SuggestionsDataService { + protected searchFindBySourceMethod = 'findBySource'; + protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; + + /** + * A private UpdateDataServiceImpl implementation to delegate specific methods to. + */ + private suggestionsDataService: SuggestionDataServiceImpl; + + /** + * A private UpdateDataServiceImpl implementation to delegate specific methods to. + */ + private suggestionSourcesDataService: SuggestionSourceDataService; + + /** + * A private UpdateDataServiceImpl implementation to delegate specific methods to. + */ + private suggestionTargetsDataService: SuggestionTargetDataService; + + private responseMsToLive = 10 * 1000; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {DefaultChangeAnalyzer} comparatorSuggestions + * @param {DefaultChangeAnalyzer} comparatorSources + * @param {DefaultChangeAnalyzer} comparatorTargets + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparatorSuggestions: DefaultChangeAnalyzer, + protected comparatorSources: DefaultChangeAnalyzer, + protected comparatorTargets: DefaultChangeAnalyzer, + ) { + this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions, this.responseMsToLive); + this.suggestionSourcesDataService = new SuggestionSourceDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources); + this.suggestionTargetsDataService = new SuggestionTargetDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorTargets); + } + + /** + * Return the list of Suggestion Sources + * + * @param options + * Find list options object. + * @return Observable>> + * The list of Suggestion Sources. + */ + public getSources(options: FindListOptions = {}): Observable>> { + return this.suggestionSourcesDataService.getSources(options); + } + + /** + * Return the list of Suggestion Target for a given source + * + * @param source + * The source for which to find targets. + * @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 Suggestion Target. + */ + public getTargets( + source: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [new RequestParam('source', source)]; + + return this.suggestionTargetsDataService.getTargets(this.searchFindBySourceMethod, options, ...linksToFollow); + } + + /** + * Return the list of Suggestion Target for a given user + * + * @param userId + * The user Id for which to find targets. + * @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 Suggestion Target. + */ + public getTargetsByUser( + userId: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [new RequestParam('target', userId)]; + return this.suggestionTargetsDataService.getTargetsByUser(userId, options, ...linksToFollow); + } + + /** + * Return a Suggestion Target for a given id + * + * @param targetId + * The target id to retrieve. + * + * @return Observable> + * The list of Suggestion Target. + */ + public getTargetById(targetId: string): Observable> { + return this.suggestionTargetsDataService.findById(targetId); + } + + /** + * Used to delete Suggestion + * @suggestionId + */ + public deleteSuggestion(suggestionId: string): Observable> { + return this.suggestionsDataService.delete(suggestionId); + } + + /** + * Return the list of Suggestion for a given target and source + * + * @param target + * The target for which to find suggestions. + * @param source + * The source for which to find suggestions. + * @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 Suggestion. + */ + public getSuggestionsByTargetAndSource( + target: string, + source: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [ + new RequestParam('target', target), + new RequestParam('source', source) + ]; + + return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, false, true, ...linksToFollow); + } + + /** + * Clear findByTargetAndSource suggestions requests from cache + */ + public clearSuggestionRequests() { + this.requestService.setStaleByHrefSubstring(this.searchFindByTargetAndSourceMethod); + } +} diff --git a/src/app/core/suggestion-notifications/target/suggestion-target-data.service.ts b/src/app/core/suggestion-notifications/target/suggestion-target-data.service.ts new file mode 100644 index 00000000000..a2f1507b100 --- /dev/null +++ b/src/app/core/suggestion-notifications/target/suggestion-target-data.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../../data/base/data-service.decorator'; + +import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +import { SuggestionTarget } from '../models/suggestion-target.model'; +import { FindAllData, FindAllDataImpl } from '../../data/base/find-all-data'; +import { Store } from '@ngrx/store'; +import { RequestService } from '../../data/request.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CoreState } from '../../core-state.model'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { FindListOptions } from '../../data/find-list-options.model'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../data/paginated-list.model'; +import { RemoteData } from '../../data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { SearchData, SearchDataImpl } from '../../data/base/search-data'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; +import { SUGGESTION_TARGET } from '../models/suggestion-target-object.resource-type'; + +@Injectable() +@dataService(SUGGESTION_TARGET) +export class SuggestionTargetDataService extends IdentifiableDataService { + + protected linkPath = 'suggestiontargets'; + private findAllData: FindAllData; + private searchData: SearchData; + protected searchFindBySourceMethod = 'findBySource'; + protected searchFindByTargetMethod = 'findByTarget'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super('suggestiontargets', requestService, rdbService, objectCache, halService); + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + /** + * Return the list of Suggestion Target for a given source + * + * @param source + * The source for which to find targets. + * @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 Suggestion Target. + */ + public getTargets( + source: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [new RequestParam('source', source)]; + + return this.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow); + } + + /** + * Return the list of Suggestion Target for a given user + * + * @param userId + * The user Id for which to find targets. + * @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 Suggestion Target. + */ + public getTargetsByUser( + userId: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [new RequestParam('target', userId)]; + + return this.searchBy(this.searchFindByTargetMethod, options, true, true, ...linksToFollow); + } + /** + * Return a Suggestion Target for a given id + * + * @param targetId + * The target id to retrieve. + * + * @return Observable> + * The list of Suggestion Target. + */ + public getTargetById(targetId: string): Observable> { + return this.findById(targetId); + } + + /** + * Make a new FindListRequest with given search method + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] 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>} + * Return an observable that emits response from the server + */ + public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + + /** + * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded + * info should be added to the objects + * + * @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>>} + * Return an observable that emits object list + */ + findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + +} diff --git a/src/app/core/suggestion-notifications/target/suggestions-target-data.service.spec.ts b/src/app/core/suggestion-notifications/target/suggestions-target-data.service.spec.ts new file mode 100644 index 00000000000..9207603a5ad --- /dev/null +++ b/src/app/core/suggestion-notifications/target/suggestions-target-data.service.spec.ts @@ -0,0 +1,138 @@ +import { TestScheduler } from 'rxjs/testing'; +import { RequestService } from '../../data/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 { RequestEntry } from '../../data/request-entry.model'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { RestResponse } from '../../cache/response.models'; +import { of as observableOf } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../../core-state.model'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { SearchData } from '../../data/base/search-data'; +import { testSearchDataImplementation } from '../../data/base/search-data.spec'; +import { SuggestionTargetDataService } from './suggestion-target-data.service'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; +import { SuggestionTarget } from '../models/suggestion-target.model'; +import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec'; +import { FindAllData } from '../../data/base/find-all-data'; +import { GetRequest } from '../../data/request.models'; +import { + createSuccessfulRemoteDataObject$ +} from '../../../shared/remote-data.utils'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { RemoteData } from '../../data/remote-data'; +import { RequestEntryState } from '../../data/request-entry-state.model'; + +describe('SuggestionTargetDataService test', () => { + let scheduler: TestScheduler; + let service: SuggestionTargetDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let http: HttpClient; + let comparator: DefaultChangeAnalyzer; + let responseCacheEntry: RequestEntry; + + const store = {} as Store; + 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 SuggestionTargetDataService( + requestService, + rdbService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + http = {} as HttpClient; + notificationsService = {} as NotificationsService; + comparator = {} as DefaultChangeAnalyzer; + 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: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(endpointURL) + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }) + }); + + + service = initTestService(); + }); + + describe('composition', () => { + const initSearchService = () => new SuggestionTargetDataService(null, null, null, null, null, null, null, null) as unknown as SearchData; + const initFindAllService = () => new SuggestionTargetDataService(null, null, null, null, null, null, null, null) as unknown as FindAllData; + testSearchDataImplementation(initSearchService); + testFindAllDataImplementation(initFindAllService); + }); + + describe('getTargetById', () => { + it('should send a new GetRequest', () => { + const expected = new GetRequest(requestService.generateRequestId(), endpointURL + '/testId'); + scheduler.schedule(() => service.getTargetById('testId').subscribe()); + scheduler.flush(); + + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + }); + + describe('getTargetsByUser', () => { + it('should send a new GetRequest', () => { + const options = { + searchParams: [new RequestParam('target', 'testId')] + }; + const searchFindByTargetMethod = 'findByTarget'; + const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/search/${searchFindByTargetMethod}?target=testId`); + scheduler.schedule(() => service.getTargetsByUser('testId', options).subscribe()); + scheduler.flush(); + + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + }); + + describe('getTargets', () => { + it('should send a new GetRequest', () => { + const options = { + searchParams: [new RequestParam('source', 'testId')] + }; + const searchFindBySourceMethod = 'findBySource'; + const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/search/${searchFindBySourceMethod}?source=testId`); + scheduler.schedule(() => service.getTargets('testId', options).subscribe()); + scheduler.flush(); + + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + }); +}); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 24c3dc5cd7a..8fb676a7247 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -2,23 +2,23 @@
@@ -74,16 +74,19 @@
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index d44817be842..60b1dffa450 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { getFirstCompletedRemoteData, } from '../../core/shared/operators'; -import { UpdateDataService } from '../../core/data/update-data.service'; import { ResourceType } from '../../core/shared/resource-type'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; @@ -22,6 +21,7 @@ import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analy import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { HALDataService } from '../../core/data/base/hal-data-service.interface'; +import { UpdateDataService } from '../../core/data/update-data.service'; @Component({ selector: 'ds-dso-edit-metadata', diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html index 4c310bd81bd..3f5af76051e 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -1,6 +1,7 @@ diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index c79d19e267c..52df841d3b6 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -32,6 +32,18 @@
+ + - +

{{'admin.registries.schema.fields.table.selected' | translate}} {{'admin.registries.schema.fields.table.id' | translate}} {{'admin.registries.schema.fields.table.field' | translate}} {{'admin.registries.schema.fields.table.scopenote' | translate}}
- + + {{field.id}} {{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}
- + diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 8cdc8ffb2d3..d1d454b2e43 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -11,7 +11,7 @@
{{'collection.select.table.selected' | translate}} {{'collection.select.table.title' | translate}}
- + diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index b608c0168a3..47e1c14d46b 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -7,15 +7,29 @@
- -
- - - - - - -
+ +
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 1bfc1dfa594..a838f51e692 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -167,7 +167,7 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Number of items per page. */ - public pageSize$; + public pageSize$: Observable; /** * Declare SortDirection enumeration to use it in the template @@ -188,7 +188,7 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Name of the field that's used to sort by */ - public sortField$; + public sortField$: Observable; public defaultSortField = 'name'; /** diff --git a/src/app/shared/resource-policies/entry/resource-policy-entry.component.html b/src/app/shared/resource-policies/entry/resource-policy-entry.component.html index a14dde366e6..9ad019c8c68 100644 --- a/src/app/shared/resource-policies/entry/resource-policy-entry.component.html +++ b/src/app/shared/resource-policies/entry/resource-policy-entry.component.html @@ -5,8 +5,8 @@ [id]="entry.id" [ngModel]="entry.checked" (ngModelChange)="this.toggleCheckbox.emit($event);"> -
{{'item.select.table.selected' | translate}} {{'item.select.table.collection' | translate}} {{'item.select.table.author' | translate}} {{'item.select.table.title' | translate}}
+ + + + + + + + + + + + + + +
{{'suggestion.evidence.score' | translate}}{{'suggestion.evidence.type' | translate}}{{'suggestion.evidence.notes' | translate}}
{{evidences[evidence].score}}{{evidence | translate}}{{evidences[evidence].notes}}
+

+
diff --git a/src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html rename to src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts b/src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts new file mode 100644 index 00000000000..60c36603f16 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { fadeIn } from '../../../shared/animations/fade'; +import { SuggestionEvidences } from '../../../core/suggestion-notifications/models/suggestion.model'; + +/** + * Show suggestion evidences such as score (authorScore, dateScore) + */ +@Component({ + selector: 'ds-suggestion-evidences', + styleUrls: [ './suggestion-evidences.component.scss' ], + templateUrl: './suggestion-evidences.component.html', + animations: [fadeIn] +}) +export class SuggestionEvidencesComponent { + + @Input() evidences: SuggestionEvidences; + +} diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.html b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.html new file mode 100644 index 00000000000..ef27876f2cb --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.html @@ -0,0 +1,45 @@ +
+
+ +
+
+ +
+
+ +
+
+
{{'suggestion.totalScore' | translate}}
+ {{ object.score }} +
+
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.scss b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.scss new file mode 100644 index 00000000000..1c522095189 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.scss @@ -0,0 +1,16 @@ +.issue-date { + color: #c8c8c8; +} + +.parent { + display: flex; + gap:10px; +} + +.import { + flex: initial; +} + +.suggestion-score { + font-size: 1.5rem; +} diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.spec.ts b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.spec.ts new file mode 100644 index 00000000000..61d1a19996c --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.spec.ts @@ -0,0 +1,81 @@ +import { SuggestionListElementComponent } from './suggestion-list-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { mockSuggestionPublicationOne } from '../../shared/mocks/publication-claim.mock'; +import { Item } from '../../core/shared/item.model'; + + +describe('SuggestionListElementComponent', () => { + let component: SuggestionListElementComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [SuggestionListElementComponent], + providers: [ + NgbModal + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionListElementComponent); + component = fixture.componentInstance; + scheduler = getTestScheduler(); + + component.object = mockSuggestionPublicationOne; + }); + + describe('SuggestionListElementComponent test', () => { + + it('should create', () => { + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + const expectedIndexableObject = Object.assign(new Item(), { + id: mockSuggestionPublicationOne.id, + metadata: mockSuggestionPublicationOne.metadata + }); + expect(component).toBeTruthy(); + expect(component.listableObject.hitHighlights).toEqual({}); + expect(component.listableObject.indexableObject).toEqual(expectedIndexableObject); + }); + + it('should check if has evidence', () => { + expect(component.hasEvidences()).toBeTruthy(); + }); + + it('should set seeEvidences', () => { + component.onSeeEvidences(true); + expect(component.seeEvidence).toBeTruthy(); + }); + + it('should emit selection', () => { + spyOn(component.selected, 'next'); + component.changeSelected({target: { checked: true}}); + expect(component.selected.next).toHaveBeenCalledWith(true); + }); + + it('should emit for deletion', () => { + spyOn(component.ignoreSuggestionClicked, 'emit'); + component.onIgnoreSuggestion('1234'); + expect(component.ignoreSuggestionClicked.emit).toHaveBeenCalledWith('1234'); + }); + + it('should emit for approve and import', () => { + const event = {collectionId:'1234', suggestion: mockSuggestionPublicationOne}; + spyOn(component.approveAndImport, 'emit'); + component.onApproveAndImport(event); + expect(component.approveAndImport.emit).toHaveBeenCalledWith(event); + }); + }); +}); diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.ts b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.ts new file mode 100644 index 00000000000..f569054bb0d --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-list-element/suggestion-list-element.component.ts @@ -0,0 +1,104 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { fadeIn } from '../../shared/animations/fade'; +import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model'; +import { Item } from '../../core/shared/item.model'; +import { isNotEmpty } from '../../shared/empty.util'; + +/** + * A simple interface to unite a specific suggestion and the id of the chosen collection + */ +export interface SuggestionApproveAndImport { + suggestion: Suggestion; + collectionId: string; +} + +/** + * Show all the suggestions by researcher + */ +@Component({ + selector: 'ds-suggestion-list-item', + styleUrls: ['./suggestion-list-element.component.scss'], + templateUrl: './suggestion-list-element.component.html', + animations: [fadeIn] +}) +export class SuggestionListElementComponent implements OnInit { + + @Input() object: Suggestion; + + @Input() isSelected = false; + + @Input() isCollectionFixed = false; + + public listableObject: any; + + public seeEvidence = false; + + /** + * The component is used to Delete suggestion + */ + @Output() ignoreSuggestionClicked = new EventEmitter(); + + /** + * The component is used to approve & import + */ + @Output() approveAndImport = new EventEmitter(); + + /** + * New value whether the element is selected + */ + @Output() selected = new EventEmitter(); + + /** + * Initialize instance variables + * + * @param {NgbModal} modalService + */ + constructor(private modalService: NgbModal) { } + + ngOnInit() { + this.listableObject = { + indexableObject: Object.assign(new Item(), {id: this.object.id, metadata: this.object.metadata}), + hitHighlights: {} + }; + } + + /** + * Approve and import the suggestion + */ + onApproveAndImport(event: SuggestionApproveAndImport) { + this.approveAndImport.emit(event); + } + + /** + * Delete the suggestion + */ + onIgnoreSuggestion(suggestionId: string) { + this.ignoreSuggestionClicked.emit(suggestionId); + } + + /** + * Change is selected value. + */ + changeSelected(event) { + this.isSelected = event.target.checked; + this.selected.next(this.isSelected); + } + + /** + * See the Evidence + */ + hasEvidences() { + return isNotEmpty(this.object.evidences); + } + + /** + * Set the see evidence variable. + */ + onSeeEvidences(seeEvidence: boolean) { + this.seeEvidence = seeEvidence; + } + +} diff --git a/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.html b/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.html new file mode 100644 index 00000000000..f2ab1e5b652 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.html @@ -0,0 +1,51 @@ +
+
+
+

{{'suggestion.title'| translate}}

+ + + + + + + +
+ + + + + + + + + + + + + +
{{'suggestion.table.name' | translate}}{{'suggestion.table.actions' | translate}}
+ {{targetElement.display}} + +
+ +
+
+
+
+
+
+
+
diff --git a/src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss b/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss rename to src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.scss diff --git a/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.ts b/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.ts new file mode 100644 index 00000000000..1c3f751ea7f --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.ts @@ -0,0 +1,143 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, take } from 'rxjs/operators'; + +import { SuggestionTarget } from '../../../core/suggestion-notifications/models/suggestion-target.model'; +import { hasValue } from '../../../shared/empty.util'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SuggestionTargetsStateService } from '../suggestion-targets.state.service'; +import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths'; +import { SuggestionsService } from '../../suggestions.service'; +import { PaginationService } from '../../../core/pagination/pagination.service'; + +/** + * Component to display the Suggestion Target list. + */ +@Component({ + selector: 'ds-publication-claim', + templateUrl: './publication-claim.component.html', + styleUrls: ['./publication-claim.component.scss'], +}) +export class PublicationClaimComponent implements OnInit { + + /** + * The source for which to list targets + */ + @Input() source: string; + + /** + * The pagination system configuration for HTML listing. + * @type {PaginationComponentOptions} + */ + public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'stp', + pageSizeOptions: [5, 10, 20, 40, 60] + }); + + /** + * The Suggestion Target list. + */ + public targets$: Observable; + /** + * The total number of Suggestion Targets. + */ + public totalElements$: Observable; + /** + * Array to track all the component subscriptions. Useful to unsubscribe them with 'onDestroy'. + * @type {Array} + */ + protected subs: Subscription[] = []; + + /** + * Initialize the component variables. + * @param {PaginationService} paginationService + * @param {SuggestionTargetsStateService} suggestionTargetsStateService + * @param {SuggestionsService} suggestionService + * @param {Router} router + */ + constructor( + private paginationService: PaginationService, + private suggestionTargetsStateService: SuggestionTargetsStateService, + private suggestionService: SuggestionsService, + private router: Router + ) { + } + + /** + * Component initialization. + */ + ngOnInit(): void { + this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(); + this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(); + + this.subs.push( + this.suggestionTargetsStateService.isSuggestionTargetsLoaded().pipe( + take(1) + ).subscribe(() => { + this.getSuggestionTargets(); + }) + ); + } + + /** + * Returns the information about the loading status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if the targets are loading, 'false' otherwise. + */ + public isTargetsLoading(): Observable { + return this.suggestionTargetsStateService.isSuggestionTargetsLoading(); + } + + /** + * Returns the information about the processing status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. + */ + public isTargetsProcessing(): Observable { + return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(); + } + + /** + * Redirect to suggestion page. + * + * @param {string} id + * the id of suggestion target + */ + public redirectToSuggestions(id: string) { + this.router.navigate([getSuggestionPageRoute(id)]); + } + + /** + * Unsubscribe from all subscriptions. + */ + ngOnDestroy(): void { + this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + + /** + * Dispatch the Suggestion Targets retrival. + */ + public getSuggestionTargets(): void { + this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( + distinctUntilChanged(), + take(1) + ).subscribe((options: PaginationComponentOptions) => { + this.suggestionTargetsStateService.dispatchRetrieveSuggestionTargets( + this.source, + options.pageSize, + options.currentPage + ); + }); + } + + public getTargetUuid(target: SuggestionTarget) { + return this.suggestionService.getTargetUuid(target); + } +} diff --git a/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.actions.ts b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.actions.ts new file mode 100644 index 00000000000..058fa367518 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.actions.ts @@ -0,0 +1,156 @@ +/* eslint-disable max-classes-per-file */ +import { Action } from '@ngrx/store'; +import { type } from '../../shared/ngrx/type'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const SuggestionTargetActionTypes = { + ADD_TARGETS: type('dspace/integration/openaire/suggestions/target/ADD_TARGETS'), + CLEAR_TARGETS: type('dspace/integration/openaire/suggestions/target/CLEAR_TARGETS'), + RETRIEVE_TARGETS_BY_SOURCE: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE'), + RETRIEVE_TARGETS_BY_SOURCE_ERROR: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE_ERROR'), + ADD_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/ADD_USER_SUGGESTIONS'), + REFRESH_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/REFRESH_USER_SUGGESTIONS'), + MARK_USER_SUGGESTIONS_AS_VISITED: type('dspace/integration/openaire/suggestions/target/MARK_USER_SUGGESTIONS_AS_VISITED') +}; + +/* tslint:disable:max-classes-per-file */ + +/** + * An ngrx action to retrieve all the Suggestion Targets. + */ +export class RetrieveTargetsBySourceAction implements Action { + type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; + payload: { + source: string; + elementsPerPage: number; + currentPage: number; + }; + + /** + * Create a new RetrieveTargetsBySourceAction. + * + * @param source + * the source for which to retrieve suggestion targets + * @param elementsPerPage + * the number of targets per page + * @param currentPage + * The page number to retrieve + */ + constructor(source: string, elementsPerPage: number, currentPage: number) { + this.payload = { + source, + elementsPerPage, + currentPage + }; + } +} + +/** + * An ngrx action for retrieving 'all Suggestion Targets' error. + */ +export class RetrieveAllTargetsErrorAction implements Action { + type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR; +} + +/** + * An ngrx action to load the Suggestion Target objects. + */ +export class AddTargetAction implements Action { + type = SuggestionTargetActionTypes.ADD_TARGETS; + payload: { + targets: SuggestionTarget[]; + totalPages: number; + currentPage: number; + totalElements: number; + }; + + /** + * Create a new AddTargetAction. + * + * @param targets + * the list of targets + * @param totalPages + * the total available pages of targets + * @param currentPage + * the current page + * @param totalElements + * the total available Suggestion Targets + */ + constructor(targets: SuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + this.payload = { + targets, + totalPages, + currentPage, + totalElements + }; + } + +} + +/** + * An ngrx action to load the user Suggestion Target object. + * Called by the ??? effect. + */ +export class AddUserSuggestionsAction implements Action { + type = SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS; + payload: { + suggestionTargets: SuggestionTarget[]; + }; + + /** + * Create a new AddUserSuggestionsAction. + * + * @param suggestionTargets + * the user suggestions target + */ + constructor(suggestionTargets: SuggestionTarget[]) { + this.payload = { suggestionTargets }; + } + +} + +/** + * An ngrx action to reload the user Suggestion Target object. + * Called by the ??? effect. + */ +export class RefreshUserSuggestionsAction implements Action { + type = SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS; +} + +/** + * An ngrx action to Mark User Suggestions As Visited. + * Called by the ??? effect. + */ +export class MarkUserSuggestionsAsVisitedAction implements Action { + type = SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED; +} + +/** + * An ngrx action to clear targets state. + */ +export class ClearSuggestionTargetsAction implements Action { + type = SuggestionTargetActionTypes.CLEAR_TARGETS; +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types. + */ +export type SuggestionTargetsActions + = AddTargetAction + | AddUserSuggestionsAction + | ClearSuggestionTargetsAction + | MarkUserSuggestionsAsVisitedAction + | RetrieveTargetsBySourceAction + | RetrieveAllTargetsErrorAction + | RefreshUserSuggestionsAction; diff --git a/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.effects.ts b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.effects.ts new file mode 100644 index 00000000000..23eedcedc5b --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.effects.ts @@ -0,0 +1,97 @@ +import { Injectable } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { TranslateService } from '@ngx-translate/core'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import { + AddTargetAction, + AddUserSuggestionsAction, + RetrieveAllTargetsErrorAction, + RetrieveTargetsBySourceAction, + SuggestionTargetActionTypes, +} from './suggestion-targets.actions'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { SuggestionsService } from '../suggestions.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; + +/** + * Provides effect methods for the Suggestion Targets actions. + */ +@Injectable() +export class SuggestionTargetsEffects { + + /** + * Retrieve all Suggestion Targets managing pagination and errors. + */ + retrieveTargetsBySource$ = createEffect(() => this.actions$.pipe( + ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE), + switchMap((action: RetrieveTargetsBySourceAction) => { + return this.suggestionsService.getTargets( + action.payload.source, + action.payload.elementsPerPage, + action.payload.currentPage + ).pipe( + map((targets: PaginatedList) => + new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements) + ), + catchError((error: Error) => { + if (error) { + console.error(error.message); + } + return of(new RetrieveAllTargetsErrorAction()); + }) + ); + }) + )); + + /** + * Show a notification on error. + */ + retrieveAllTargetsErrorAction$ = createEffect(() => this.actions$.pipe( + ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR), + tap(() => { + this.notificationsService.error(null, this.translate.get('suggestion.target.error.service.retrieve')); + }) + ), { dispatch: false }); + + /** + * Fetch the current user suggestion + */ + refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe( + ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), + switchMap(() => { + return this.store$.select((state: any) => state.core.auth.userId) + .pipe( + switchMap((userId: string) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(userId) + .pipe( + map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), + catchError((errors) => of(errors)) + ); + }), + catchError((errors) => of(errors)) + ); + })) + ); + + /** + * Initialize the effect class variables. + * @param {Actions} actions$ + * @param {Store} store$ + * @param {TranslateService} translate + * @param {NotificationsService} notificationsService + * @param {SuggestionsService} suggestionsService + */ + constructor( + private actions$: Actions, + private store$: Store, + private translate: TranslateService, + private notificationsService: NotificationsService, + private suggestionsService: SuggestionsService + ) { + } +} diff --git a/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.reducer.ts b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.reducer.ts new file mode 100644 index 00000000000..09ead019d11 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.reducer.ts @@ -0,0 +1,100 @@ +import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; + +/** + * The interface representing the OpenAIRE suggestion targets state. + */ +export interface SuggestionTargetState { + targets: SuggestionTarget[]; + processing: boolean; + loaded: boolean; + totalPages: number; + currentPage: number; + totalElements: number; + currentUserTargets: SuggestionTarget[]; + currentUserTargetsVisited: boolean; +} + +/** + * Used for the OpenAIRE Suggestion Target state initialization. + */ +const SuggestionTargetInitialState: SuggestionTargetState = { + targets: [], + processing: false, + loaded: false, + totalPages: 0, + currentPage: 0, + totalElements: 0, + currentUserTargets: null, + currentUserTargetsVisited: false +}; + +/** + * The OpenAIRE Broker Topic Reducer + * + * @param state + * the current state initialized with SuggestionTargetInitialState + * @param action + * the action to perform on the state + * @return SuggestionTargetState + * the new state + */ +export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { + switch (action.type) { + case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { + return Object.assign({}, state, { + targets: [], + processing: true + }); + } + + case SuggestionTargetActionTypes.ADD_TARGETS: { + return Object.assign({}, state, { + targets: state.targets.concat(action.payload.targets), + processing: false, + loaded: true, + totalPages: action.payload.totalPages, + currentPage: state.currentPage, + totalElements: action.payload.totalElements + }); + } + + case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { + return Object.assign({}, state, { + targets: [], + processing: false, + loaded: true, + totalPages: 0, + currentPage: 0, + totalElements: 0, + }); + } + + case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { + return Object.assign({}, state, { + currentUserTargets: action.payload.suggestionTargets + }); + } + + case SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED: { + return Object.assign({}, state, { + currentUserTargetsVisited: true + }); + } + + case SuggestionTargetActionTypes.CLEAR_TARGETS: { + return Object.assign({}, state, { + targets: [], + processing: false, + loaded: false, + totalPages: 0, + currentPage: 0, + totalElements: 0, + }); + } + + default: { + return state; + } + } +} diff --git a/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.state.service.ts b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.state.service.ts new file mode 100644 index 00000000000..7e0e6ad8368 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion-targets/suggestion-targets.state.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from '@angular/core'; + +import { select, Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { + getCurrentUserSuggestionTargetsSelector, + getCurrentUserSuggestionTargetsVisitedSelector, + getSuggestionTargetCurrentPageSelector, + getSuggestionTargetTotalsSelector, + isSuggestionTargetLoadedSelector, + isReciterSuggestionTargetProcessingSelector, + suggestionTargetObjectSelector +} from '../selectors'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +import { + ClearSuggestionTargetsAction, + MarkUserSuggestionsAsVisitedAction, + RefreshUserSuggestionsAction, + RetrieveTargetsBySourceAction +} from './suggestion-targets.actions'; +import { SuggestionNotificationsState } from '../../notifications/notifications.reducer'; + +/** + * The service handling the Suggestion targets State. + */ +@Injectable() +export class SuggestionTargetsStateService { + + /** + * Initialize the service variables. + * @param {Store} store + */ + constructor(private store: Store) { } + + /** + * Returns the list of Suggestion Targets from the state. + * + * @return Observable + * The list of Suggestion Targets. + */ + public getSuggestionTargets(): Observable { + return this.store.pipe(select(suggestionTargetObjectSelector())); + } + + /** + * Returns the information about the loading status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if the targets are loading, 'false' otherwise. + */ + public isSuggestionTargetsLoading(): Observable { + return this.store.pipe( + select(isSuggestionTargetLoadedSelector), + map((loaded: boolean) => !loaded) + ); + } + + /** + * Returns the information about the loading status of the Suggestion Targets (whether or not they were loaded). + * + * @return Observable + * 'true' if the targets are loaded, 'false' otherwise. + */ + public isSuggestionTargetsLoaded(): Observable { + return this.store.pipe(select(isSuggestionTargetLoadedSelector)); + } + + /** + * Returns the information about the processing status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. + */ + public isSuggestionTargetsProcessing(): Observable { + return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector)); + } + + /** + * Returns, from the state, the total available pages of the Suggestion Targets. + * + * @return Observable + * The number of the Suggestion Targets pages. + */ + public getSuggestionTargetsTotalPages(): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector)); + } + + /** + * Returns the current page of the Suggestion Targets, from the state. + * + * @return Observable + * The number of the current Suggestion Targets page. + */ + public getSuggestionTargetsCurrentPage(): Observable { + return this.store.pipe(select(getSuggestionTargetCurrentPageSelector)); + } + + /** + * Returns the total number of the Suggestion Targets. + * + * @return Observable + * The number of the Suggestion Targets. + */ + public getSuggestionTargetsTotals(): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector)); + } + + /** + * Dispatch a request to change the Suggestion Targets state, retrieving the targets from the server. + * + * @param source + * the source for which to retrieve suggestion targets + * @param elementsPerPage + * The number of the targets per page. + * @param currentPage + * The number of the current page. + */ + public dispatchRetrieveSuggestionTargets(source: string, elementsPerPage: number, currentPage: number): void { + this.store.dispatch(new RetrieveTargetsBySourceAction(source, elementsPerPage, currentPage)); + } + + /** + * Returns, from the state, the suggestion targets for the current user. + * + * @return Observable + * The Suggestion Targets object. + */ + public getCurrentUserSuggestionTargets(): Observable { + return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); + } + + /** + * Returns, from the state, whether or not the user has consulted their suggestion targets. + * + * @return Observable + * True if user already visited, false otherwise. + */ + public hasUserVisitedSuggestions(): Observable { + return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector)); + } + + /** + * Dispatch a new MarkUserSuggestionsAsVisitedAction + */ + public dispatchMarkUserSuggestionsAsVisitedAction(): void { + this.store.dispatch(new MarkUserSuggestionsAsVisitedAction()); + } + + /** + * Dispatch an action to clear the Reciter Suggestion Targets state. + */ + public dispatchClearSuggestionTargetsAction(): void { + this.store.dispatch(new ClearSuggestionTargetsAction()); + } + + /** + * Dispatch an action to refresh the user suggestions. + */ + public dispatchRefreshUserSuggestionsAction(): void { + this.store.dispatch(new RefreshUserSuggestionsAction()); + } +} diff --git a/src/app/suggestion-notifications/suggestion.service.spec.ts b/src/app/suggestion-notifications/suggestion.service.spec.ts new file mode 100644 index 00000000000..268eb59ddd1 --- /dev/null +++ b/src/app/suggestion-notifications/suggestion.service.spec.ts @@ -0,0 +1,183 @@ +import { SuggestionsService } from './suggestions.service'; +import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service'; +import { + SuggestionsDataService +} from '../core/suggestion-notifications/suggestions-data.service'; + + +import { + SuggestionTargetDataService +} from '../core/suggestion-notifications/target/suggestion-target-data.service'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { FindListOptions } from '../core/data/find-list-options.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { ResearcherProfile } from '../core/profile/model/researcher-profile.model'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { mockSuggestionPublicationOne } from '../shared/mocks/publication-claim.mock'; +import { ResourceType } from '../core/shared/resource-type'; + + +import { + SuggestionTarget +} from '../core/suggestion-notifications/models/suggestion-target.model'; + +describe('SuggestionsService test', () => { + let scheduler: TestScheduler; + let service: SuggestionsService; + let researcherProfileService: ResearcherProfileDataService; + let suggestionsDataService: SuggestionsDataService; + let suggestionTargetDataService: SuggestionTargetDataService; + let translateService: any = { + instant: (str) => str, + }; + const suggestionTarget = { + id: '1234:4321', + display: 'display', + source: 'source', + total: 8, + type: new ResourceType('suggestiontarget') + }; + + const mockResercherProfile = { + id: '1234', + uuid: '1234', + visible: true + }; + + function initTestService() { + return new SuggestionsService( + researcherProfileService, + suggestionsDataService, + suggestionTargetDataService, + translateService + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + findById: createSuccessfulRemoteDataObject$(mockResercherProfile as ResearcherProfile), + findRelatedItemId: observableOf('1234'), + }); + + suggestionTargetDataService = jasmine.createSpyObj('suggestionTargetsDataService', { + getTargets: observableOf(null), + findById: observableOf(null), + }); + + suggestionsDataService = jasmine.createSpyObj('suggestionsDataService', { + searchBy: observableOf(null), + delete: observableOf(null), + deleteSuggestion: createSuccessfulRemoteDataObject$({}), + getSuggestionsByTargetAndSource : observableOf(null), + clearSuggestionRequests : null, + getTargetsByUser: observableOf(null), + }); + + service = initTestService(); + + }); + + describe('Suggestion service', () => { + it('should create', () => { + expect(service).toBeDefined(); + }); + + it('should get targets', () => { + const sortOptions = new SortOptions('display', SortDirection.ASC); + const findListOptions: FindListOptions = { + elementsPerPage: 10, + currentPage: 1, + sort: sortOptions + }; + service.getTargets('source', 10, 1); + expect(suggestionTargetDataService.getTargets).toHaveBeenCalledWith('source', findListOptions); + }); + + it('should get suggestions', () => { + const sortOptions = new SortOptions('display', SortDirection.ASC); + const findListOptions: FindListOptions = { + elementsPerPage: 10, + currentPage: 1, + sort: sortOptions + }; + service.getSuggestions('source:target', 10, 1, sortOptions); + expect(suggestionsDataService.getSuggestionsByTargetAndSource).toHaveBeenCalledWith('target', 'source', findListOptions); + }); + + it('should clear suggestions', () => { + service.clearSuggestionRequests(); + expect(suggestionsDataService.clearSuggestionRequests).toHaveBeenCalled(); + }); + + it('should delete reviewed suggestion', () => { + service.deleteReviewedSuggestion('1234'); + expect(suggestionsDataService.deleteSuggestion).toHaveBeenCalledWith('1234'); + }); + + it('should retrieve current user suggestions', () => { + service.retrieveCurrentUserSuggestions('1234'); + expect(researcherProfileService.findById).toHaveBeenCalledWith('1234', true); + }); + + it('should approve and import suggestion', () => { + spyOn(service, 'resolveCollectionId'); + const workspaceitemService = {importExternalSourceEntry: (x,y) => observableOf(null)}; + service.approveAndImport(workspaceitemService as unknown as WorkspaceitemDataService, mockSuggestionPublicationOne, '1234'); + expect(service.resolveCollectionId).toHaveBeenCalled(); + }); + + it('should approve and import suggestions', () => { + spyOn(service, 'approveAndImport'); + const workspaceitemService = {importExternalSourceEntry: (x,y) => observableOf(null)}; + service.approveAndImportMultiple(workspaceitemService as unknown as WorkspaceitemDataService, [mockSuggestionPublicationOne], '1234'); + expect(service.approveAndImport).toHaveBeenCalledWith(workspaceitemService as unknown as WorkspaceitemDataService, mockSuggestionPublicationOne, '1234'); + }); + + it('should delete suggestion', () => { + spyOn(service, 'deleteReviewedSuggestion').and.returnValue(createSuccessfulRemoteDataObject$({})); + service.ignoreSuggestion('1234'); + expect(service.deleteReviewedSuggestion).toHaveBeenCalledWith('1234'); + }); + + it('should delete suggestions', () => { + spyOn(service, 'ignoreSuggestion'); + service.ignoreSuggestionMultiple([mockSuggestionPublicationOne]); + expect(service.ignoreSuggestion).toHaveBeenCalledWith(mockSuggestionPublicationOne.id); + }); + + it('should get target Uuid', () => { + expect(service.getTargetUuid(suggestionTarget as SuggestionTarget)).toBe('4321'); + expect(service.getTargetUuid({id: ''} as SuggestionTarget)).toBe(null); + }); + + it('should get suggestion interpolation', () => { + const result = service.getNotificationSuggestionInterpolation(suggestionTarget as SuggestionTarget); + expect(result.count).toEqual(suggestionTarget.total); + expect(result.source).toEqual('suggestion.source.' + suggestionTarget.source); + expect(result.type).toEqual('suggestion.type.' + suggestionTarget.source); + expect(result.suggestionId).toEqual(suggestionTarget.id); + expect(result.displayName).toEqual(suggestionTarget.display); + }); + + it('should translate suggestion type', () => { + expect(service.translateSuggestionType('source')).toEqual('suggestion.type.source'); + }); + + it('should translate suggestion source', () => { + expect(service.translateSuggestionSource('source')).toEqual('suggestion.source.source'); + }); + + it('should resolve collection id', () => { + expect(service.resolveCollectionId(mockSuggestionPublicationOne, '1234')).toEqual('1234'); + }); + + it('should check if collection is fixed', () => { + expect(service.isCollectionFixed([mockSuggestionPublicationOne])).toBeFalse(); + }); + }); +}); diff --git a/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.html b/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.html new file mode 100644 index 00000000000..838bdb95ad4 --- /dev/null +++ b/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.html @@ -0,0 +1,9 @@ + + +
+
+ {{ 'notification.suggestion.please' | translate }} + {{ 'notification.suggestion.review' | translate}} +
+
+
diff --git a/src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.html b/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.html rename to src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.scss diff --git a/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.ts b/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.ts new file mode 100644 index 00000000000..357a6ca34ef --- /dev/null +++ b/src/app/suggestion-notifications/suggestions-notification/suggestions-notification.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; +import { SuggestionsService } from '../suggestions.service'; +import { Observable } from 'rxjs'; + +/** + * Show suggestions notification, used on myDSpace and Profile pages + */ +@Component({ + selector: 'ds-suggestions-notification', + templateUrl: './suggestions-notification.component.html', + styleUrls: ['./suggestions-notification.component.scss'] +}) +export class SuggestionsNotificationComponent implements OnInit { + + /** + * The user suggestion targets. + */ + suggestionsRD$: Observable; + + constructor( + private suggestionTargetsStateService: SuggestionTargetsStateService, + private suggestionsService: SuggestionsService + ) { } + + ngOnInit() { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.suggestionsRD$ = this.suggestionTargetsStateService.getCurrentUserSuggestionTargets(); + } + + /** + * Interpolated params to build the notification suggestions notification. + * @param suggestionTarget + */ + public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any { + return this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget); + } + +} diff --git a/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.html b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.html new file mode 100644 index 00000000000..474dfa329e7 --- /dev/null +++ b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.html @@ -0,0 +1,27 @@ +
+ +
+ + diff --git a/src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.scss b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.scss rename to src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.scss diff --git a/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.spec.ts b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.spec.ts new file mode 100644 index 00000000000..c8d59115d81 --- /dev/null +++ b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.spec.ts @@ -0,0 +1,77 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SuggestionsPopupComponent } from './suggestions-popup.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { of as observableOf } from 'rxjs'; +import { mockSuggestionTargetsObjectOne } from '../../shared/mocks/publication-claim-targets.mock'; +import { SuggestionsService } from '../suggestions.service'; + +describe('SuggestionsPopupComponent', () => { + let component: SuggestionsPopupComponent; + let fixture: ComponentFixture; + + const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', { + hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'), + getCurrentUserSuggestionTargets: jasmine.createSpy('getCurrentUserSuggestionTargets'), + dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction'), + dispatchRefreshUserSuggestionsAction: jasmine.createSpy('dispatchRefreshUserSuggestionsAction') + }); + + const mockNotificationInterpolation = { count: 12, source: 'source', suggestionId: 'id', displayName: 'displayName' }; + const suggestionService = jasmine.createSpyObj('SuggestionService', { + getNotificationSuggestionInterpolation: + jasmine.createSpy('getNotificationSuggestionInterpolation').and.returnValue(mockNotificationInterpolation) + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ SuggestionsPopupComponent ], + providers: [ + { provide: SuggestionTargetsStateService, useValue: suggestionStateService }, + { provide: SuggestionsService, useValue: suggestionService }, + ], + schemas: [NO_ERRORS_SCHEMA] + + }) + .compileComponents(); + })); + + describe('should create', () => { + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionsPopupComponent); + component = fixture.componentInstance; + spyOn(component, 'initializePopup').and.returnValue(null); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(component.initializePopup).toHaveBeenCalled(); + }); + + }); + + describe('when there are publication suggestions', () => { + + beforeEach(() => { + suggestionStateService.hasUserVisitedSuggestions.and.returnValue(observableOf(false)); + suggestionStateService.getCurrentUserSuggestionTargets.and.returnValue(observableOf([mockSuggestionTargetsObjectOne])); + suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction.and.returnValue(observableOf(null)); + suggestionStateService.dispatchRefreshUserSuggestionsAction.and.returnValue(observableOf(null)); + + fixture = TestBed.createComponent(SuggestionsPopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should show a notification when new publication suggestions are available', () => { + expect(suggestionStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction).toHaveBeenCalled(); + }); + + }); +}); diff --git a/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.ts b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.ts new file mode 100644 index 00000000000..2cf3db128e6 --- /dev/null +++ b/src/app/suggestion-notifications/suggestions-popup/suggestions-popup.component.ts @@ -0,0 +1,82 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; +import { SuggestionsService } from '../suggestions.service'; +import { take, takeUntil } from 'rxjs/operators'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { combineLatest, Observable, of, Subject } from 'rxjs'; +import { trigger } from '@angular/animations'; + + +import { fromTopEnter } from '../../shared/animations/fromTop'; + +/** + * Show suggestions on a popover window, used on the homepage + */ +@Component({ + selector: 'ds-suggestions-popup', + templateUrl: './suggestions-popup.component.html', + styleUrls: ['./suggestions-popup.component.scss'], + animations: [ + trigger('enterLeave', [ + fromTopEnter + ]) + ], +}) +export class SuggestionsPopupComponent implements OnInit, OnDestroy { + + labelPrefix = 'notification.'; + + subscription; + + suggestionsRD$: Observable; + + + constructor( + private suggestionTargetsStateService: SuggestionTargetsStateService, + private suggestionsService: SuggestionsService + ) { } + + ngOnInit() { + this.initializePopup(); + } + + public initializePopup() { + const notifier = new Subject(); + this.subscription = combineLatest([ + this.suggestionTargetsStateService.getCurrentUserSuggestionTargets().pipe(take(2)), + this.suggestionTargetsStateService.hasUserVisitedSuggestions() + ]).pipe(takeUntil(notifier)).subscribe(([suggestions, visited]) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + if (isNotEmpty(suggestions)) { + if (!visited) { + this.suggestionsRD$ = of(suggestions); + this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); + notifier.next(null); + notifier.complete(); + } + } + }); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + /** + * Interpolated params to build the notification suggestions notification. + * @param suggestionTarget + */ + public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any { + return this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget); + } + + /** + * Hide popup from view + */ + public removePopup() { + this.suggestionsRD$ = null; + } +} diff --git a/src/app/suggestion-notifications/suggestions.service.ts b/src/app/suggestion-notifications/suggestions.service.ts new file mode 100644 index 00000000000..874659eab5f --- /dev/null +++ b/src/app/suggestion-notifications/suggestions.service.ts @@ -0,0 +1,306 @@ +import { Injectable } from '@angular/core'; + +import { of, forkJoin, Observable } from 'rxjs'; +import { catchError, map, mergeMap, take } from 'rxjs/operators'; + +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { RemoteData } from '../core/data/remote-data'; +import { PaginatedList } from '../core/data/paginated-list.model'; +import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { ResearcherProfile } from '../core/profile/model/researcher-profile.model'; +import { + getAllSucceededRemoteDataPayload, + getFinishedRemoteData, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload +} from '../core/shared/operators'; +import { Suggestion } from '../core/suggestion-notifications/models/suggestion.model'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NoContent } from '../core/shared/NoContent.model'; +import { environment } from '../../environments/environment'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import {FindListOptions} from '../core/data/find-list-options.model'; +import {SuggestionConfig} from '../../config/suggestion-config.interfaces'; +import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service'; +import { + SuggestionTargetDataService +} from '../core/suggestion-notifications/target/suggestion-target-data.service'; +import { + SuggestionsDataService +} from '../core/suggestion-notifications/suggestions-data.service'; +import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths'; + +/** + * useful for multiple approvals and ignores operation + * */ +export interface SuggestionBulkResult { + success: number; + fails: number; +} + +/** + * The service handling all Suggestion Target requests to the REST service. + */ +@Injectable() +export class SuggestionsService { + + /** + * Initialize the service variables. + * @param {ResearcherProfileDataService} researcherProfileService + * @param {SuggestionTargetDataService} suggestionTargetDataService + * @param {SuggestionsDataService} suggestionsDataService + * @param translateService + */ + constructor( + private researcherProfileService: ResearcherProfileDataService, + private suggestionsDataService: SuggestionsDataService, + private suggestionTargetDataService: SuggestionTargetDataService, + private translateService: TranslateService + ) { + } + + /** + * Return the list of Suggestion Target managing pagination and errors. + * + * @param source + * The source for which to retrieve targets + * @param elementsPerPage + * The number of the target per page + * @param currentPage + * The page number to retrieve + * @return Observable> + * The list of Suggestion Targets. + */ + public getTargets(source, elementsPerPage, currentPage): Observable> { + const sortOptions = new SortOptions('display', SortDirection.ASC); + + const findListOptions: FindListOptions = { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + sort: sortOptions + }; + + return this.suggestionTargetDataService.getTargets(source, findListOptions).pipe( + getFinishedRemoteData(), + take(1), + map((rd: RemoteData>) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + throw new Error('Can\'t retrieve Suggestion Target from the Search Target REST service'); + } + }) + ); + } + + /** + * Return the list of review suggestions Target managing pagination and errors. + * + * @param targetId + * The target id for which to find suggestions. + * @param elementsPerPage + * The number of the target per page + * @param currentPage + * The page number to retrieve + * @param sortOptions + * The sort options + * @return Observable>> + * The list of Suggestion. + */ + public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable> { + const [source, target] = targetId.split(':'); + + const findListOptions: FindListOptions = { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + sort: sortOptions + }; + + return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions).pipe( + getAllSucceededRemoteDataPayload() + ); + } + + /** + * Clear suggestions requests from cache + */ + public clearSuggestionRequests() { + this.suggestionsDataService.clearSuggestionRequests(); + } + + /** + * Used to delete Suggestion + * @suggestionId + */ + public deleteReviewedSuggestion(suggestionId: string): Observable> { + return this.suggestionsDataService.deleteSuggestion(suggestionId).pipe( + map((response: RemoteData) => { + if (response.isSuccess) { + return response; + } else { + throw new Error('Can\'t delete Suggestion from the Search Target REST service'); + } + }), + take(1) + ); + } + + /** + * Retrieve suggestion targets for the given user + * + * @param userUuid + * The EPerson id for which to retrieve suggestion targets + */ + public retrieveCurrentUserSuggestions(userUuid: string): Observable { + return this.researcherProfileService.findById(userUuid, true).pipe( + getFirstSucceededRemoteDataPayload(), + mergeMap((profile: ResearcherProfile) => { + if (isNotEmpty(profile)) { + return this.researcherProfileService.findRelatedItemId(profile).pipe( + mergeMap((itemId: string) => { + return this.suggestionsDataService.getTargetsByUser(itemId).pipe( + getFirstSucceededRemoteListPayload() + ); + }) + ); + } else { + return of([]); + } + }), + take(1) + ); + } + + /** + * Perform the approve and import operation over a single suggestion + * @param suggestion target suggestion + * @param collectionId the collectionId + * @param workspaceitemService injected dependency + * @private + */ + public approveAndImport(workspaceitemService: WorkspaceitemDataService, + suggestion: Suggestion, + collectionId: string): Observable { + + const resolvedCollectionId = this.resolveCollectionId(suggestion, collectionId); + return workspaceitemService.importExternalSourceEntry(suggestion.externalSourceUri, resolvedCollectionId) + .pipe( + getFirstSucceededRemoteDataPayload(), + catchError(() => of(null)) + ); + } + + /** + * Perform the delete operation over a single suggestion. + * @param suggestionId + */ + public ignoreSuggestion(suggestionId): Observable> { + return this.deleteReviewedSuggestion(suggestionId).pipe( + catchError(() => of(null)) + ); + } + + /** + * Perform a bulk approve and import operation. + * @param workspaceitemService injected dependency + * @param suggestions the array containing the suggestions + * @param collectionId the collectionId + */ + public approveAndImportMultiple(workspaceitemService: WorkspaceitemDataService, + suggestions: Suggestion[], + collectionId: string): Observable { + + return forkJoin(suggestions.map((suggestion: Suggestion) => + this.approveAndImport(workspaceitemService, suggestion, collectionId))) + .pipe(map((results: WorkspaceItem[]) => { + return { + success: results.filter((result) => result != null).length, + fails: results.filter((result) => result == null).length + }; + }), take(1)); + } + + /** + * Perform a bulk ignoreSuggestion operation. + * @param suggestions the array containing the suggestions + */ + public ignoreSuggestionMultiple(suggestions: Suggestion[]): Observable { + return forkJoin(suggestions.map((suggestion: Suggestion) => this.ignoreSuggestion(suggestion.id))) + .pipe(map((results: RemoteData[]) => { + return { + success: results.filter((result) => result != null).length, + fails: results.filter((result) => result == null).length + }; + }), take(1)); + } + + /** + * Get the researcher uuid (for navigation purpose) from a target instance. + * TODO Find a better way + * @param target + * @return the researchUuid + */ + public getTargetUuid(target: SuggestionTarget): string { + const tokens = target.id.split(':'); + return tokens.length === 2 ? tokens[1] : null; + } + + /** + * Interpolated params to build the notification suggestions notification. + * @param suggestionTarget + */ + public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any { + return { + count: suggestionTarget.total, + source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)), + type: this.translateService.instant(this.translateSuggestionType(suggestionTarget.source)), + suggestionId: suggestionTarget.id, + displayName: suggestionTarget.display, + url: getSuggestionPageRoute(suggestionTarget.id) + }; + } + + public translateSuggestionType(source: string): string { + return 'suggestion.type.' + source; + } + + public translateSuggestionSource(source: string): string { + return 'suggestion.source.' + source; + } + + /** + * If the provided collectionId ha no value, tries to resolve it by suggestion source. + * @param suggestion + * @param collectionId + */ + public resolveCollectionId(suggestion: Suggestion, collectionId): string { + if (hasValue(collectionId)) { + return collectionId; + } + return environment.suggestion + .find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source) + .collectionId; + } + + /** + * Return true if all the suggestion are configured with the same fixed collection + * in the configuration. + * @param suggestions + */ + public isCollectionFixed(suggestions: Suggestion[]): boolean { + return this.getFixedCollectionIds(suggestions).length === 1; + } + + private getFixedCollectionIds(suggestions: Suggestion[]): string[] { + const collectionIds = {}; + suggestions.forEach((suggestion: Suggestion) => { + const conf = environment.suggestion.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source); + if (hasValue(conf)) { + collectionIds[conf.collectionId] = true; + } + }); + return Object.keys(collectionIds); + } +} diff --git a/src/app/suggestions-page/suggestions-page-routing-paths.ts b/src/app/suggestions-page/suggestions-page-routing-paths.ts new file mode 100644 index 00000000000..0f3aa782d21 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routing-paths.ts @@ -0,0 +1,11 @@ +import { URLCombiner } from '../core/url-combiner/url-combiner'; + +export const SUGGESTION_MODULE_PATH = 'suggestions'; + +export function getSuggestionModuleRoute() { + return `/${SUGGESTION_MODULE_PATH}`; +} + +export function getSuggestionPageRoute(SuggestionId: string) { + return new URLCombiner(getSuggestionModuleRoute(), SuggestionId).toString(); +} diff --git a/src/app/suggestions-page/suggestions-page-routing.module.ts b/src/app/suggestions-page/suggestions-page-routing.module.ts new file mode 100644 index 00000000000..f7d4ecf9556 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routing.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { SuggestionsPageResolver } from './suggestions-page.resolver'; +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { PublicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: ':targetId', + resolve: { + suggestionTargets: SuggestionsPageResolver, + breadcrumb: PublicationClaimBreadcrumbResolver//I18nBreadcrumbResolver + }, + data: { + title: 'admin.notifications.publicationclaim.page.title', + breadcrumbKey: 'admin.notifications.publicationclaim', + showBreadcrumbsFluid: false + }, + canActivate: [AuthenticatedGuard], + runGuardsAndResolvers: 'always', + component: SuggestionsPageComponent, + }, + ]) + ], + providers: [ + SuggestionsPageResolver, + PublicationClaimBreadcrumbResolver + ] +}) +export class SuggestionsPageRoutingModule { + +} diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html new file mode 100644 index 00000000000..4e367b638e8 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -0,0 +1,48 @@ +
+
+
+ +
+ +

+ {{'suggestion.suggestionFor' | translate}} + {{researcherName}} + {{'suggestion.from.source' | translate}} {{ translateSuggestionSource() | translate }} +

+ +
+ + ({{ getSelectedSuggestionsCount() }}) + + +
+ + +
    +
  • + +
  • +
+
+
+
{{ 'suggestion.count.missing' | translate }}
+
+
+
+
diff --git a/src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/suggestions-page/suggestions-page.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html rename to src/app/suggestions-page/suggestions-page.component.scss diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts new file mode 100644 index 00000000000..98535297a29 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -0,0 +1,214 @@ +import { CommonModule } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { + SuggestionApproveAndImport, + SuggestionListElementComponent +} from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionsService } from '../suggestion-notifications/suggestions.service'; +import { getMockSuggestionNotificationsStateService, getMockSuggestionsService } from '../shared/mocks/suggestion.mock'; +import { mockSuggestionPublicationOne, mockSuggestionPublicationTwo } from '../shared/mocks/publication-claim.mock'; +import { SuggestionEvidencesComponent } from '../suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; +import { ObjectKeysPipe } from '../shared/utils/object-keys-pipe'; +import { VarDirective } from '../shared/utils/var.directive'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterStub } from '../shared/testing/router.stub'; +import { mockSuggestionTargetsObjectOne } from '../shared/mocks/publication-claim-targets.mock'; +import { AuthService } from '../core/auth/auth.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; +import { getMockTranslateService } from '../shared/mocks/translate.service.mock'; +import { SuggestionTargetsStateService } from '../suggestion-notifications/suggestion-targets/suggestion-targets.state.service'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { createSuccessfulRemoteDataObject } from '../shared/remote-data.utils'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; +import { PaginationService } from '../core/pagination/pagination.service'; + +describe('SuggestionPageComponent', () => { + let component: SuggestionsPageComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + const mockSuggestionsService = getMockSuggestionsService(); + const mockSuggestionsTargetStateService = getMockSuggestionNotificationsStateService(); + const router = new RouterStub(); + const routeStub = { + data: observableOf({ + suggestionTargets: createSuccessfulRemoteDataObject(mockSuggestionTargetsObjectOne) + }), + queryParams: observableOf({}) + }; + const workspaceitemServiceMock = jasmine.createSpyObj('WorkspaceitemDataService', { + importExternalSourceEntry: jasmine.createSpy('importExternalSourceEntry') + }); + + const authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + setRedirectUrl: {} + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + TranslateModule.forRoot() + ], + declarations: [ + SuggestionEvidencesComponent, + SuggestionListElementComponent, + SuggestionsPageComponent, + ObjectKeysPipe, + VarDirective + ], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: WorkspaceitemDataService, useValue: workspaceitemServiceMock }, + { provide: Router, useValue: router }, + { provide: SuggestionsService, useValue: mockSuggestionsService }, + { provide: SuggestionTargetsStateService, useValue: mockSuggestionsTargetStateService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: TranslateService, useValue: getMockTranslateService() }, + { provide: PaginationService, useValue: paginationService }, + SuggestionsPageComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents().then(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionsPageComponent); + component = fixture.componentInstance; + scheduler = getTestScheduler(); + }); + + it('should create', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(component).toBeTruthy(); + expect(component.suggestionId).toBe(mockSuggestionTargetsObjectOne.id); + expect(component.researcherName).toBe(mockSuggestionTargetsObjectOne.display); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should update page on pagination change', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + component.onPaginationChange(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should update suggestion on page update', (done) => { + spyOn(component.processing$, 'next'); + spyOn(component.suggestionsRD$, 'next'); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + paginationService.getFindListOptions().subscribe(() => { + expect(component.processing$.next).toHaveBeenCalled(); + expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); + expect(component.suggestionsRD$.next).toHaveBeenCalled(); + expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); + done(); + }); + component.updatePage(); + }); + + it('should flag suggestion for deletion', fakeAsync(() => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + component.ignoreSuggestion('1'); + expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + tick(201); + expect(component.updatePage).toHaveBeenCalled(); + })); + + it('should flag all suggestion for deletion', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + component.ignoreSuggestionAllSelected(); + expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should approve and import', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + component.approveAndImport({collectionId: '1234'} as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should approve and import multiple suggestions', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + component.approveAndImportAllSelected({collectionId: '1234'} as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should select and deselect suggestion', () => { + component.selectedSuggestions = {}; + component.onSelected(mockSuggestionPublicationOne, true); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toBe(mockSuggestionPublicationOne); + component.onSelected(mockSuggestionPublicationOne, false); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toBeUndefined(); + }); + + it('should toggle all suggestions', () => { + component.selectedSuggestions = {}; + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toEqual(mockSuggestionPublicationOne); + expect(component.selectedSuggestions[mockSuggestionPublicationTwo.id]).toEqual(mockSuggestionPublicationTwo); + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.selectedSuggestions).toEqual({}); + }); + + it('should return all selected suggestions count', () => { + component.selectedSuggestions = {}; + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.getSelectedSuggestionsCount()).toEqual(2); + }); + + it('should check if all collection is fixed', () => { + component.isCollectionFixed([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(mockSuggestionsService.isCollectionFixed).toHaveBeenCalled(); + }); + + it('should translate suggestion source', () => { + component.translateSuggestionSource(); + expect(mockSuggestionsService.translateSuggestionSource).toHaveBeenCalled(); + }); + + it('should translate suggestion type', () => { + component.translateSuggestionType(); + expect(mockSuggestionsService.translateSuggestionType).toHaveBeenCalled(); + }); +}); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts new file mode 100644 index 00000000000..fdbf131e592 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -0,0 +1,286 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, Router } from '@angular/router'; + +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { SortDirection, SortOptions, } from '../core/cache/models/sort-options.model'; +import { PaginatedList } from '../core/data/paginated-list.model'; +import { RemoteData } from '../core/data/remote-data'; +import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { SuggestionBulkResult, SuggestionsService } from '../suggestion-notifications/suggestions.service'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { Suggestion } from '../core/suggestion-notifications/models/suggestion.model'; +import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model'; +import { AuthService } from '../core/auth/auth.service'; +import { SuggestionApproveAndImport } from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { SuggestionTargetsStateService } from '../suggestion-notifications/suggestion-targets/suggestion-targets.state.service'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import {FindListOptions} from '../core/data/find-list-options.model'; +import {redirectOn4xx} from '../core/shared/authorized.operators'; +import { + getWorkspaceItemEditRoute +} from '../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; + +@Component({ + selector: 'ds-suggestion-page', + templateUrl: './suggestions-page.component.html', + styleUrls: ['./suggestions-page.component.scss'], +}) + +/** + * Component used to visualize one of the suggestions from the publication claim page or from the notification pop up + */ + +export class SuggestionsPageComponent implements OnInit { + + /** + * The pagination configuration + */ + paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'sp', + pageSizeOptions: [5, 10, 20, 40, 60] + }); + + /** + * The sorting configuration + */ + paginationSortConfig: SortOptions = new SortOptions('trust', SortDirection.DESC); + + /** + * The FindListOptions object + */ + defaultConfig: FindListOptions = Object.assign(new FindListOptions(), {sort: this.paginationSortConfig}); + + /** + * A boolean representing if results are loading + */ + public processing$ = new BehaviorSubject(false); + + /** + * A list of remote data objects of suggestions + */ + suggestionsRD$: BehaviorSubject> = new BehaviorSubject>({} as any); + + targetRD$: Observable>; + targetId$: Observable; + + suggestionTarget: SuggestionTarget; + suggestionId: any; + suggestionSource: any; + researcherName: any; + researcherUuid: any; + + selectedSuggestions: { [id: string]: Suggestion } = {}; + isBulkOperationPending = false; + + constructor( + private authService: AuthService, + private notificationService: NotificationsService, + private paginationService: PaginationService, + private route: ActivatedRoute, + private router: Router, + private suggestionService: SuggestionsService, + private suggestionTargetsStateService: SuggestionTargetsStateService, + private translateService: TranslateService, + private workspaceItemService: WorkspaceitemDataService + ) { + } + + ngOnInit(): void { + this.targetRD$ = this.route.data.pipe( + map((data: Data) => data.suggestionTargets as RemoteData), + redirectOn4xx(this.router, this.authService) + ); + + this.targetId$ = this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((target: SuggestionTarget) => target.id) + ); + this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((suggestionTarget: SuggestionTarget) => { + this.suggestionTarget = suggestionTarget; + this.suggestionId = suggestionTarget.id; + this.researcherName = suggestionTarget.display; + this.suggestionSource = suggestionTarget.source; + this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); + this.updatePage(); + }); + + this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); + } + + /** + * Called when one of the pagination settings is changed + */ + onPaginationChange() { + this.updatePage(); + } + + /** + * Update the list of suggestions + */ + updatePage() { + this.processing$.next(true); + const pageConfig$: Observable = this.paginationService.getFindListOptions( + this.paginationOptions.id, + this.defaultConfig, + ).pipe( + distinctUntilChanged() + ); + combineLatest([this.targetId$, pageConfig$]).pipe( + switchMap(([targetId, config]: [string, FindListOptions]) => { + return this.suggestionService.getSuggestions( + targetId, + config.elementsPerPage, + config.currentPage, + config.sort + ); + }), + take(1) + ).subscribe((results: PaginatedList) => { + this.processing$.next(false); + this.suggestionsRD$.next(results); + this.suggestionService.clearSuggestionRequests(); + }); + } + + /** + * Used to delete a suggestion. + * @suggestionId + */ + ignoreSuggestion(suggestionId) { + this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + //We add a little delay in the page refresh so that we ensure the deletion has been propagated + setTimeout(() => this.updatePage(), 200); + }); + } + + /** + * Used to delete all selected suggestions. + */ + ignoreSuggestionAllSelected() { + this.isBulkOperationPending = true; + this.suggestionService + .ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) + .subscribe((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('suggestion.ignoreSuggestion.bulk.success', + {count: results.success})); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('suggestion.ignoreSuggestion.bulk.error', + {count: results.fails})); + } + }); + } + + /** + * Used to approve & import. + * @param event contains the suggestion and the target collection + */ + approveAndImport(event: SuggestionApproveAndImport) { + this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) + .subscribe((workspaceitem: WorkspaceItem) => { + const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); + this.notificationService.success('', content, {timeOut:0}, true); + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + }); + } + + /** + * Used to approve & import all selected suggestions. + * @param event contains the target collection + */ + approveAndImportAllSelected(event: SuggestionApproveAndImport) { + this.isBulkOperationPending = true; + this.suggestionService + .approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) + .subscribe((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('suggestion.approveAndImport.bulk.success', + {count: results.success})); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('suggestion.approveAndImport.bulk.error', + {count: results.fails})); + } + }); + } + + /** + * When a specific suggestion is selected. + * @param object the suggestions + * @param selected the new selected value for the suggestion + */ + onSelected(object: Suggestion, selected: boolean) { + if (selected) { + this.selectedSuggestions[object.id] = object; + } else { + delete this.selectedSuggestions[object.id]; + } + } + + /** + * When Toggle Select All occurs. + * @param suggestions all the visible suggestions inside the page + */ + onToggleSelectAll(suggestions: Suggestion[]) { + if ( this.getSelectedSuggestionsCount() > 0) { + this.selectedSuggestions = {}; + } else { + suggestions.forEach((suggestion) => { + this.selectedSuggestions[suggestion.id] = suggestion; + }); + } + } + + /** + * The current number of selected suggestions. + */ + getSelectedSuggestionsCount(): number { + return Object.keys(this.selectedSuggestions).length; + } + + /** + * Return true if all the suggestion are configured with the same fixed collection in the configuration. + * @param suggestions + */ + isCollectionFixed(suggestions: Suggestion[]): boolean { + return this.suggestionService.isCollectionFixed(suggestions); + } + + /** + * Label to be used to translate the suggestion source. + */ + translateSuggestionSource() { + return this.suggestionService.translateSuggestionSource(this.suggestionSource); + } + + /** + * Label to be used to translate the suggestion type. + */ + translateSuggestionType() { + return this.suggestionService.translateSuggestionType(this.suggestionSource); + } + +} diff --git a/src/app/suggestions-page/suggestions-page.module.ts b/src/app/suggestions-page/suggestions-page.module.ts new file mode 100644 index 00000000000..e25e483e39a --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { SharedModule } from '../shared/shared.module'; +import { SuggestionsPageRoutingModule } from './suggestions-page-routing.module'; +import { SuggestionsService } from '../suggestion-notifications/suggestions.service'; +import { SuggestionsDataService } from '../core/suggestion-notifications/suggestions-data.service'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@NgModule({ + declarations: [SuggestionsPageComponent], + imports: [ + CommonModule, + SharedModule, + SuggestionsPageRoutingModule, + NotificationsModule + ], + providers: [ + SuggestionsDataService, + SuggestionsService + ] +}) +export class SuggestionsPageModule { } diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts new file mode 100644 index 00000000000..dde5b847f5d --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { find } from 'rxjs/operators'; + +import { RemoteData } from '../core/data/remote-data'; +import { hasValue } from '../shared/empty.util'; +import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model'; +import { + SuggestionTargetDataService +} from '../core/suggestion-notifications/target/suggestion-target-data.service'; + +/** + * This class represents a resolver that requests a specific collection before the route is activated + */ +@Injectable() +export class SuggestionsPageResolver implements Resolve> { + constructor(private suggestionsDataService: SuggestionTargetDataService) { + } + + /** + * Method for resolving a suggestion target based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( + find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.html b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.html deleted file mode 100644 index 0904d0fcde5..00000000000 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts index 2c12b07589f..3188d00891e 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts @@ -3,12 +3,14 @@ import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-acti import { Router } from '@angular/router'; import { RouterStub } from '../../../shared/testing/router.stub'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive'; +import { DynamicComponentLoaderDirective } from '../../../shared/abstract-component-loader/dynamic-component-loader.directive'; import { rendersAdvancedWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; import { By } from '@angular/platform-browser'; import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths'; +import { ThemeService } from 'src/app/shared/theme-support/theme.service'; +import { getMockThemeService } from 'src/app/shared/mocks/theme-service.mock'; const ADVANCED_WORKFLOW_ACTION_TEST = 'testaction'; @@ -17,17 +19,20 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => { let fixture: ComponentFixture; let router: RouterStub; + let themeService: ThemeService; beforeEach(async () => { router = new RouterStub(); + themeService = getMockThemeService(); await TestBed.configureTestingModule({ declarations: [ - AdvancedWorkflowActionsDirective, + DynamicComponentLoaderDirective, AdvancedWorkflowActionsLoaderComponent, ], providers: [ { provide: Router, useValue: router }, + { provide: ThemeService, useValue: themeService }, ], }).overrideComponent(AdvancedWorkflowActionsLoaderComponent, { set: { @@ -50,24 +55,24 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => { describe('When the component is rendered', () => { it('should display the AdvancedWorkflowActionTestComponent when the type has been defined in a rendersAdvancedWorkflowTaskOption', () => { - spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(AdvancedWorkflowActionTestComponent); + spyOn(component, 'getComponent').and.returnValue(AdvancedWorkflowActionTestComponent); component.ngOnInit(); fixture.detectChanges(); - expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(ADVANCED_WORKFLOW_ACTION_TEST); + expect(component.getComponent).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('#AdvancedWorkflowActionsLoaderComponent'))).not.toBeNull(); expect(router.navigate).not.toHaveBeenCalled(); }); it('should redirect to page not found when the type has not been defined in a rendersAdvancedWorkflowTaskOption', () => { - spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(undefined); + spyOn(component, 'getComponent').and.returnValue(undefined); component.type = 'nonexistingaction'; component.ngOnInit(); fixture.detectChanges(); - expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith('nonexistingaction'); + expect(component.getComponent).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]); }); }); diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts index 32f14c015de..1db49b97e8d 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts @@ -1,21 +1,22 @@ -import { Component, Input, ViewChild, ComponentFactoryResolver, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { hasValue } from '../../../shared/empty.util'; import { getAdvancedComponentByWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; -import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive'; import { Router } from '@angular/router'; import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths'; +import { GenericConstructor } from '../../../core/shared/generic-constructor'; +import { AbstractComponentLoaderComponent } from 'src/app/shared/abstract-component-loader/abstract-component-loader.component'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; /** * Component for loading a {@link AdvancedWorkflowActionComponent} depending on the "{@link type}" input */ @Component({ selector: 'ds-advanced-workflow-actions-loader', - templateUrl: './advanced-workflow-actions-loader.component.html', - styleUrls: ['./advanced-workflow-actions-loader.component.scss'], + templateUrl: '../../../shared/abstract-component-loader/abstract-component-loader.component.html', }) -export class AdvancedWorkflowActionsLoaderComponent implements OnInit { +export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoaderComponent implements OnInit { /** * The name of the type to render @@ -23,35 +24,28 @@ export class AdvancedWorkflowActionsLoaderComponent implements OnInit { */ @Input() type: string; - /** - * Directive to determine where the dynamic child component is located - */ - @ViewChild(AdvancedWorkflowActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedWorkflowActionsDirective; + protected inputNames: (keyof this & string)[] = [ + ...this.inputNames, + 'type', + ]; constructor( - private componentFactoryResolver: ComponentFactoryResolver, + protected themeService: ThemeService, private router: Router, ) { + super(themeService); } - /** - * Fetch, create and initialize the relevant component - */ ngOnInit(): void { - const comp = this.getComponentByWorkflowTaskOption(this.type); - if (hasValue(comp)) { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp); - - const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef; - viewContainerRef.clear(); - viewContainerRef.createComponent(componentFactory); + if (hasValue(this.getComponent())) { + super.ngOnInit(); } else { void this.router.navigate([PAGE_NOT_FOUND_PATH]); } } - getComponentByWorkflowTaskOption(type: string): any { - return getAdvancedComponentByWorkflowTaskOption(type); + public getComponent(): GenericConstructor { + return getAdvancedComponentByWorkflowTaskOption(this.type); } } diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts index 326eebe4a79..a4d4a0f2c93 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts @@ -28,9 +28,14 @@ export function getWorkspaceItemDeleteRoute(wsiId: string) { return new URLCombiner(getWorkspaceItemModuleRoute(), wsiId, WORKSPACE_ITEM_DELETE_PATH).toString(); } +export function getWorkspaceItemEditRoute(wsiId: string) { + return new URLCombiner(getWorkspaceItemModuleRoute(), wsiId, WORKSPACE_ITEM_EDIT_PATH).toString(); +} + export const WORKFLOW_ITEM_EDIT_PATH = 'edit'; export const WORKFLOW_ITEM_DELETE_PATH = 'delete'; export const WORKFLOW_ITEM_VIEW_PATH = 'view'; export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback'; export const ADVANCED_WORKFLOW_PATH = 'advanced'; export const WORKSPACE_ITEM_DELETE_PATH = 'delete'; +export const WORKSPACE_ITEM_EDIT_PATH = 'edit'; diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts index cf998c52743..c2bd3d5ad76 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -23,9 +23,6 @@ import { import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; -import { - AdvancedWorkflowActionsDirective -} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive'; import { AccessControlModule } from '../access-control/access-control.module'; import { ReviewersListComponent @@ -54,7 +51,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; AdvancedWorkflowActionRatingComponent, AdvancedWorkflowActionSelectReviewerComponent, AdvancedWorkflowActionPageComponent, - AdvancedWorkflowActionsDirective, ReviewersListComponent, ] }) diff --git a/src/assets/i18n/ar.json5 b/src/assets/i18n/ar.json5 index 3069104dd9a..617d2a93ded 100644 --- a/src/assets/i18n/ar.json5 +++ b/src/assets/i18n/ar.json5 @@ -4284,9 +4284,9 @@ // TODO New key - Add a translation "mydspace.status.mydspaceValidation": "Validation", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // TODO New key - Add a translation - "mydspace.status.mydspaceWaitingController": "Waiting for controller", + "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // "mydspace.status.mydspaceWorkflow": "Workflow", // TODO New key - Add a translation diff --git a/src/assets/i18n/bn.json5 b/src/assets/i18n/bn.json5 index c70cc6f4595..c9c5ba26402 100644 --- a/src/assets/i18n/bn.json5 +++ b/src/assets/i18n/bn.json5 @@ -3886,7 +3886,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "বৈধতা", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "নিয়ামক জন্য অপেক্ষা করছে", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/ca.json5 b/src/assets/i18n/ca.json5 index ad8fe49424e..db624f2c3f7 100644 --- a/src/assets/i18n/ca.json5 +++ b/src/assets/i18n/ca.json5 @@ -4196,7 +4196,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validació", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Esperant el controlador", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 7f9583a50eb..9816571e057 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -4195,9 +4195,9 @@ // TODO New key - Add a translation "mydspace.status.mydspaceValidation": "Validation", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // TODO New key - Add a translation - "mydspace.status.mydspaceWaitingController": "Waiting for controller", + "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // "mydspace.status.mydspaceWorkflow": "Workflow", // TODO New key - Add a translation diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index c185a13432b..4f9988acb68 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -731,9 +731,6 @@ // "admin.workflow.title": "Administer Workflow", "admin.workflow.title": "Workflows verwalten", - // "admin.workflow.item.workflow": "Workflow", - "admin.workflow.item.workflow": "Workflows", - // "admin.workflow.item.delete": "Delete", "admin.workflow.item.delete": "Löschen", @@ -2193,6 +2190,44 @@ // "info.privacy.title": "Privacy Statement", "info.privacy.title": "Datenschutzerklärung", + // "info.feedback.breadcrumbs": "Feedback", + "info.feedback.breadcrumbs": "Feedback", + + // "info.feedback.head": "Feedback", + "info.feedback.head": "Feedback", + + // "info.feedback.title": "Feedback", + "info.feedback.title": "Feedback", + + // "info.feedback.info": "Thanks for sharing your feedback about the DSpace system. Your comments are appreciated!", + "info.feedback.info": "Wir freuen uns auf Ihr Feedback. Sofern Sie eine Rückmeldung wünschen, werden wir uns in Kürze bei Ihnen melden.", + + // "info.feedback.email_help": "This address will be used to follow up on your feedback.", + "info.feedback.email_help": "Sollten wir Rückfragen zu Ihrem Feedback haben, dann kontaktieren wir Sie unter dieser E-Mailadresse.", + + // "info.feedback.send": "Send Feedback", + "info.feedback.send": "Feedback senden", + + // "info.feedback.comments": "Comments", + "info.feedback.comments": "Ihr Kommentar", + + // "info.feedback.email-label": "Your Email", + "info.feedback.email-label": "Ihre E-Mailadresse", + + // "info.feedback.create.success": "Feedback Sent Successfully!", + "info.feedback.create.success": "Vielen Dank für Ihr Feedback! Ihr Feedback wurde erfolgreich versendet.", + + // "info.feedback.error.email.required": "A valid email address is required", + "info.feedback.error.email.required": "Bitte geben Sie eine gültige E-Mailadresse ein!", + + // "info.feedback.error.message.required": "A comment is required", + "info.feedback.error.message.required": "Bitte geben Sie einen Kommentar ein!", + + // "info.feedback.page-label": "Page", + "info.feedback.page-label": "URL der Seite", + + // "info.feedback.page_help": "The page related to your feedback", + "info.feedback.page_help": "Dies ist die URL der Seite, auf die sich Ihr Feedback bezieht.", // "item.alerts.private": "This item is private", @@ -3491,7 +3526,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validierung", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Warten auf die Überprüfung", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5b3bd292622..6c68cae19c5 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4,15 +4,15 @@ "401.link.home-page": "Take me to the home page", - "401.unauthorized": "unauthorized", + "401.unauthorized": "Unauthorized", "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", "403.link.home-page": "Take me to the home page", - "403.forbidden": "forbidden", + "403.forbidden": "Forbidden", - "500.page-internal-server-error": "Service Unavailable", + "500.page-internal-server-error": "Service unavailable", "500.help": "The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.", @@ -22,15 +22,15 @@ "404.link.home-page": "Take me to the home page", - "404.page-not-found": "page not found", + "404.page-not-found": "Page not found", - "error-page.description.401": "unauthorized", + "error-page.description.401": "Unauthorized", - "error-page.description.403": "forbidden", + "error-page.description.403": "Forbidden", - "error-page.description.500": "Service Unavailable", + "error-page.description.500": "Service unavailable", - "error-page.description.404": "page not found", + "error-page.description.404": "Page not found", "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", @@ -58,7 +58,7 @@ "admin.registries.bitstream-formats.create.failure.head": "Failure", - "admin.registries.bitstream-formats.create.head": "Create Bitstream format", + "admin.registries.bitstream-formats.create.head": "Create bitstream format", "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", @@ -128,6 +128,8 @@ "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.selected": "Selected bitstream formats", + "admin.registries.bitstream-formats.table.id": "ID", "admin.registries.bitstream-formats.table.return": "Back", @@ -142,7 +144,9 @@ "admin.registries.bitstream-formats.title": "Bitstream Format Registry", - "admin.registries.bitstream-formats.select": "Select bitstream format", + "admin.registries.bitstream-formats.select": "Select", + + "admin.registries.bitstream-formats.deselect": "Deselect", "admin.registries.metadata.breadcrumbs": "Metadata registry", @@ -160,8 +164,14 @@ "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", + "admin.registries.metadata.schemas.select": "Select", + + "admin.registries.metadata.schemas.deselect": "Deselect", + "admin.registries.metadata.schemas.table.delete": "Delete selected", + "admin.registries.metadata.schemas.table.selected": "Selected schemas", + "admin.registries.metadata.schemas.table.id": "ID", "admin.registries.metadata.schemas.table.name": "Name", @@ -174,6 +184,10 @@ "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", + "admin.registries.schema.fields.select": "Select", + + "admin.registries.schema.fields.deselect": "Deselect", + "admin.registries.schema.fields.head": "Schema metadata fields", "admin.registries.schema.fields.no-items": "No metadata fields to show.", @@ -182,6 +196,8 @@ "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.selected": "Selected metadata fields", + "admin.registries.schema.fields.table.id": "ID", "admin.registries.schema.fields.table.scopenote": "Scope Note", @@ -266,7 +282,7 @@ "admin.access-control.epeople.search.scope.metadata": "Metadata", - "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.search.scope.email": "Email (exact)", "admin.access-control.epeople.search.button": "Search", @@ -278,7 +294,7 @@ "admin.access-control.epeople.table.name": "Name", - "admin.access-control.epeople.table.email": "E-mail (exact)", + "admin.access-control.epeople.table.email": "Email (exact)", "admin.access-control.epeople.table.edit": "Edit", @@ -298,9 +314,9 @@ "admin.access-control.epeople.form.lastName": "Last name", - "admin.access-control.epeople.form.email": "E-mail", + "admin.access-control.epeople.form.email": "Email", - "admin.access-control.epeople.form.emailHint": "Must be valid e-mail address", + "admin.access-control.epeople.form.emailHint": "Must be a valid email address", "admin.access-control.epeople.form.canLogIn": "Can log in", @@ -600,9 +616,9 @@ "admin.metadata-import.page.error.addFile": "Select file first!", - "admin.metadata-import.page.error.addFileUrl": "Insert file url first!", + "admin.metadata-import.page.error.addFileUrl": "Insert file URL first!", - "admin.batch-import.page.error.addFile": "Select Zip file first!", + "admin.batch-import.page.error.addFile": "Select ZIP file first!", "admin.metadata-import.page.toggle.upload": "Upload", @@ -760,7 +776,7 @@ "bitstream-request-a-copy.name.error": "The name is required", - "bitstream-request-a-copy.email.label": "Your e-mail address *", + "bitstream-request-a-copy.email.label": "Your email address *", "bitstream-request-a-copy.email.hint": "This email address is used for sending the file.", @@ -808,6 +824,8 @@ "browse.metadata.title": "Title", + "browse.metadata.srsc": "Subject Category", + "browse.metadata.author.breadcrumbs": "Browse by Author", "browse.metadata.dateissued.breadcrumbs": "Browse by Date", @@ -874,13 +892,11 @@ "browse.startsWith.input": "Filter", - "browse.taxonomy_srsc.title": "Browsing by Subject Category", - "browse.taxonomy.button": "Browse", - "browse.title": "Browsing {{ collection }} by {{ field }}{{ startsWith }} {{ value }}", + "browse.title": "Browsing by {{ field }}{{ startsWith }} {{ value }}", - "browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}", + "browse.title.page": "Browsing by {{ field }} {{ value }}", "search.browse.item-back": "Back to Results", @@ -892,6 +908,8 @@ "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", + "collection.browse.logo": "Browse for a collection logo", + "collection.create.head": "Create a Collection", "collection.create.notifications.success": "Successfully created the Collection", @@ -1088,6 +1106,8 @@ "collection.listelement.badge": "Collection", + "collection.logo": "Collection logo", + "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.empty": "No items to show", @@ -1104,6 +1124,8 @@ "collection.select.empty": "No collections to show", + "collection.select.table.selected": "Selected collections", + "collection.select.table.select": "Select collection", "collection.select.table.deselect": "Deselect collection", @@ -1168,6 +1190,12 @@ "communityList.showMore": "Show More", + "communityList.expand": "Expand {{ name }}", + + "communityList.collapse": "Collapse {{ name }}", + + "community.browse.logo": "Browse for a community logo", + "community.create.head": "Create a Community", "community.create.notifications.success": "Successfully created the Community", @@ -1202,9 +1230,9 @@ "community.edit.logo.label": "Community logo", - "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", + "community.edit.logo.notifications.add.error": "Uploading community logo failed. Please verify the content before retrying.", - "community.edit.logo.notifications.add.success": "Upload Community logo successful.", + "community.edit.logo.notifications.add.success": "Upload community logo successful.", "community.edit.logo.notifications.delete.success.title": "Logo deleted", @@ -1212,13 +1240,13 @@ "community.edit.logo.notifications.delete.error.title": "Error deleting logo", - "community.edit.logo.upload": "Drop a Community Logo to upload", + "community.edit.logo.upload": "Drop a community logo to upload", "community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.unauthorized": "You do not have privileges to make this change", - "community.edit.notifications.error": "An error occured while editing the Community", + "community.edit.notifications.error": "An error occured while editing the community", "community.edit.return": "Back", @@ -1244,6 +1272,8 @@ "community.listelement.badge": "Community", + "community.logo": "Community logo", + "comcol-role.edit.no-group": "None", "comcol-role.edit.create": "Create", @@ -1612,9 +1642,9 @@ "error.validation.required": "This field is required", - "error.validation.NotValidEmail": "This E-mail is not a valid email", + "error.validation.NotValidEmail": "This is not a valid email", - "error.validation.emailTaken": "This E-mail is already taken", + "error.validation.emailTaken": "This email is already taken", "error.validation.groupExists": "This group already exists", @@ -1690,7 +1720,7 @@ "forgot-password.form.label.passwordrepeat": "Retype to confirm", - "forgot-password.form.error.empty-password": "Please enter a password in the box below.", + "forgot-password.form.error.empty-password": "Please enter a password in the boxes above.", "forgot-password.form.error.matching-passwords": "The passwords do not match.", @@ -1768,12 +1798,12 @@ "form.create": "Create", - "form.repeatable.sort.tip": "Drop the item in the new position", - "form.number-picker.decrement": "Decrement {{field}}", "form.number-picker.increment": "Increment {{field}}", + "form.repeatable.sort.tip": "Drop the item in the new position", + "grant-deny-request-copy.deny": "Don't send copy", "grant-deny-request-copy.email.back": "Back", @@ -2142,6 +2172,8 @@ "item.edit.metadata.headers.value": "Value", + "item.edit.metadata.metadatafield": "Edit field", + "item.edit.metadata.metadatafield.error": "An error occurred validating the metadata field", "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", @@ -2478,6 +2510,8 @@ "item.page.claim.tooltip": "Claim this item as profile", + "item.page.image.alt.ROR": "ROR logo", + "item.preview.dc.identifier.uri": "Identifier:", "item.preview.dc.contributor.author": "Authors:", @@ -2530,10 +2564,26 @@ "item.preview.oaire.fundingStream": "Funding Stream:", + "item.preview.oairecerif.identifier.url": "URL", + + "item.preview.organization.address.addressCountry": "Country", + + "item.preview.organization.foundingDate": "Founding Date", + + "item.preview.organization.identifier.crossrefid": "CrossRef ID", + + "item.preview.organization.identifier.isni": "ISNI", + + "item.preview.organization.identifier.ror": "ROR ID", + + "item.preview.organization.legalName": "Legal Name", + "item.select.confirm": "Confirm selected", "item.select.empty": "No items to show", + "item.select.table.selected": "Selected items", + "item.select.table.select": "Select item", "item.select.table.deselect": "Deselect item", @@ -2664,6 +2714,8 @@ "itemtemplate.edit.metadata.headers.value": "Value", + "itemtemplate.edit.metadata.metadatafield": "Edit field", + "itemtemplate.edit.metadata.metadatafield.error": "An error occurred validating the metadata field", "itemtemplate.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", @@ -2730,6 +2782,8 @@ "journalissue.page.titleprefix": "Journal Issue: ", + "journalissue.search.results.head": "Journal Issue Search Results", + "journalvolume.listelement.badge": "Journal Volume", "journalvolume.page.description": "Description", @@ -2742,6 +2796,8 @@ "journalvolume.page.volume": "Volume", + "journalvolume.search.results.head": "Journal Volume Search Results", + "iiifsearchable.listelement.badge": "Document Media", "iiifsearchable.page.titleprefix": "Document: ", @@ -2950,7 +3006,7 @@ "menu.section.quality-assurance": "Quality Assurance", - "menu.section.notifications_reciter": "Publication Claim", + "menu.section.notifications_publication-claim": "Publication Claim", "menu.section.pin": "Pin sidebar", @@ -3050,13 +3106,13 @@ "mydspace.results.no-title": "No title", - "mydspace.results.no-uri": "No Uri", + "mydspace.results.no-uri": "No URI", - "mydspace.search-form.placeholder": "Search in mydspace...", + "mydspace.search-form.placeholder": "Search in MyDSpace...", "mydspace.show.workflow": "Workflow tasks", - "mydspace.show.workspace": "Your Submissions", + "mydspace.show.workspace": "Your submissions", "mydspace.show.supervisedWorkspace": "Supervised items", @@ -3064,7 +3120,7 @@ "mydspace.status.mydspaceValidation": "Validation", - "mydspace.status.mydspaceWaitingController": "Waiting for controller", + "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWorkflow": "Workflow", @@ -3082,6 +3138,14 @@ "mydspace.view-btn": "View", + "mydspace.import": "Import", + + "notification.suggestion": "We found {{count}} publications in the {{source}} that seems to be related to your profile.
", + + "notification.suggestion.review": "review the suggestions", + + "notification.suggestion.please": "Please", + "nav.browse.header": "All of DSpace", "nav.community-browse.header": "By Community", @@ -3092,7 +3156,7 @@ "nav.login": "Log In", - "nav.user-profile-menu-and-logout": "User profile menu and Log Out", + "nav.user-profile-menu-and-logout": "User profile menu and log out", "nav.logout": "Log Out", @@ -3268,6 +3332,8 @@ "orgunit.page.titleprefix": "Organizational Unit: ", + "orgunit.page.ror": "ROR Identifier", + "pagination.options.description": "Pagination options", "pagination.results-per-page": "Results Per Page", @@ -3474,8 +3540,6 @@ "profile.special.groups.head": "Authorization special groups you belong to", - "profile.head": "Update Profile", - "profile.metadata.form.error.firstname.required": "First Name is required", "profile.metadata.form.error.lastname.required": "Last Name is required", @@ -3576,6 +3640,72 @@ "media-viewer.playlist": "Playlist", + "suggestion.loading": "Loading ...", + + "suggestion.title": "Publication Claim", + + "suggestion.title.breadcrumbs": "Publication Claim", + + "suggestion.targets.description": "Below you can see all the suggestions ", + + "suggestion.targets": "Current Suggestions", + + "suggestion.table.name": "Researcher Name", + + "suggestion.table.actions": "Actions", + + "suggestion.button.review": "Review {{ total }} suggestion(s)", + + "suggestion.button.review.title": "Review {{ total }} suggestion(s) for ", + + "suggestion.noTargets": "No target found.", + + "suggestion.target.error.service.retrieve": "An error occurred while loading the Suggestion targets", + + "suggestion.evidence.type": "Type", + + "suggestion.evidence.score": "Score", + + "suggestion.evidence.notes": "Notes", + + "suggestion.approveAndImport": "Approve & import", + + "suggestion.approveAndImport.success": "The suggestion has been imported successfully. View.", + + "suggestion.approveAndImport.bulk": "Approve & import Selected", + + "suggestion.approveAndImport.bulk.success": "{{ count }} suggestions have been imported successfully ", + + "suggestion.approveAndImport.bulk.error": "{{ count }} suggestions haven't been imported due to unexpected server errors", + + "suggestion.ignoreSuggestion": "Ignore Suggestion", + + "suggestion.ignoreSuggestion.success": "The suggestion has been discarded", + + "suggestion.ignoreSuggestion.bulk": "Ignore Suggestion Selected", + + "suggestion.ignoreSuggestion.bulk.success": "{{ count }} suggestions have been discarded ", + + "suggestion.ignoreSuggestion.bulk.error": "{{ count }} suggestions haven't been discarded due to unexpected server errors", + + "suggestion.seeEvidence": "See evidence", + + "suggestion.hideEvidence": "Hide evidence", + + "suggestion.suggestionFor": "Suggestions for", + + "suggestion.suggestionFor.breadcrumb": "Suggestions for {{ name }}", + + "suggestion.source.openaire": "OpenAIRE Graph", + + "suggestion.from.source": "from the ", + + "suggestion.count.missing": "You have no publication claims left", + + "suggestion.totalScore": "Total Score", + + "suggestion.type.openaire": "OpenAIRE", + "register-email.title": "New user registration", "register-page.create-profile.header": "Create Profile", @@ -3672,10 +3802,14 @@ "relationships.isIssueOf": "Journal Issues", + "relationships.isIssueOf.JournalIssue": "Journal Issue", + "relationships.isJournalIssueOf": "Journal Issue", "relationships.isJournalOf": "Journals", + "relationships.isJournalVolumeOf": "Journal Volume", + "relationships.isOrgUnitOf": "Organizational Units", "relationships.isPersonOf": "Authors", @@ -3692,6 +3826,8 @@ "relationships.isVolumeOf": "Journal Volumes", + "relationships.isVolumeOf.JournalVolume": "Journal Volume", + "relationships.isContributorOf": "Contributors", "relationships.isContributorOf.OrgUnit": "Contributor (Organizational Unit)", @@ -4042,7 +4178,7 @@ "search.filters.namedresourcetype.Validation": "Validation", - "search.filters.namedresourcetype.Waiting for Controller": "Waiting for Controller", + "search.filters.namedresourcetype.Waiting for Controller": "Waiting for reviewer", "search.filters.namedresourcetype.Workflow": "Workflow", @@ -4128,6 +4264,18 @@ "sorting.lastModified.DESC": "Last modified Descending", + "sorting.person.familyName.ASC": "Surname Ascending", + + "sorting.person.familyName.DESC": "Surname Descending", + + "sorting.person.givenName.ASC": "Name Ascending", + + "sorting.person.givenName.DESC": "Name Descending", + + "sorting.person.birthDate.ASC": "Birth Date Ascending", + + "sorting.person.birthDate.DESC": "Birth Date Descending", + "statistics.title": "Statistics", "statistics.header": "Statistics for {{ scope }}", @@ -4252,6 +4400,8 @@ "submission.import-external.source.lcname": "Library of Congress Names", + "submission.import-external.source.ror": "Research Organization Registry (ROR)", + "submission.import-external.preview.title": "Item Preview", "submission.import-external.preview.title.Publication": "Publication Preview", @@ -4344,6 +4494,8 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.head.arxiv": "Importing from arXiv", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.ror": "Import from ROR", + "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal", @@ -4366,6 +4518,12 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isOrgUnitOfProject.title": "Import Remote Organization", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.isOrgUnitOfProject.added.local-entity": "Successfully added local organization to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.isOrgUnitOfProject.added.new-entity": "Successfully imported and added external organization to the selection", + "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", @@ -4422,6 +4580,18 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.arxiv": "arXiv ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.ror": "ROR ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidWorks": "ORCID ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.crossref": "CrossRef ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.scopus": "Scopus ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.openaireFunding": "Funding OpenAIRE ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournalIssn": "Sherpa Journals by ISSN ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", @@ -4436,6 +4606,8 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.isPublicationOfAuthor": "Publication of the Author", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isOrgUnitOfProject": "OrgUnit of the Project", + "submission.sections.describe.relationship-lookup.selection-tab.title.openAIREFunding": "Funding OpenAIRE API", "submission.sections.describe.relationship-lookup.selection-tab.title.isProjectOfPublication": "Project", @@ -4448,6 +4620,8 @@ "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", + "submission.sections.describe.relationship-lookup.title.Journal": "Journal", + "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", @@ -4482,6 +4656,8 @@ "submission.sections.describe.relationship-lookup.title.isPublicationOfAuthor": "Publication", + "submission.sections.describe.relationship-lookup.title.isOrgUnitOfProject": "OrgUnit", + "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", @@ -4544,6 +4720,8 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.wos": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.ror": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title": "Search Results", "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don't you can still use it for this submission.", @@ -4586,6 +4764,8 @@ "submission.sections.general.no-collection": "No collection found", + "submission.sections.general.no-entity": "No entity types found", + "submission.sections.general.no-sections": "No options available", "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", @@ -5014,6 +5194,8 @@ "supervision.search.results.head": "Workflow and Workspace tasks", + "orgunit.search.results.head": "Organizational Unit Search Results", + "workflow-item.edit.breadcrumbs": "Edit workflowitem", "workflow-item.edit.title": "Edit workflowitem", @@ -5474,4 +5656,7 @@ "vocabulary-treeview.search.form.add": "Add", + "admin.notifications.publicationclaim.breadcrumbs": "Publication Claim", + + "admin.notifications.publicationclaim.page.title": "Publication Claim", } diff --git a/src/assets/i18n/es.json5 b/src/assets/i18n/es.json5 index 0d5b27473cf..dc4e3afcc70 100644 --- a/src/assets/i18n/es.json5 +++ b/src/assets/i18n/es.json5 @@ -4524,7 +4524,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validación", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Esperando al controlador", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index 423099b956f..5fec62d2b99 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -4479,7 +4479,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validointi", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Odottaa tarkastajaa", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 699ca5cc27b..80566e589ea 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -3828,7 +3828,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "En cours de validation", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "En attente d'assignation", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/gd.json5 b/src/assets/i18n/gd.json5 index 55a53bc6f1b..929ab87d108 100644 --- a/src/assets/i18n/gd.json5 +++ b/src/assets/i18n/gd.json5 @@ -3873,7 +3873,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Dearbhadh", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "A' feitheamh riaghladair", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/hu.json5 b/src/assets/i18n/hu.json5 index d186f6435a7..afbfa25a136 100644 --- a/src/assets/i18n/hu.json5 +++ b/src/assets/i18n/hu.json5 @@ -4989,7 +4989,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Érvényesítés", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Várakozás a kontrollerre", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/it.json5 b/src/assets/i18n/it.json5 index 7f410ce0b17..ad2478ae615 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -4570,7 +4570,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Convalida", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "In attesa del controllo", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/ja.json5 b/src/assets/i18n/ja.json5 index da2385fd62f..03323eb7ef4 100644 --- a/src/assets/i18n/ja.json5 +++ b/src/assets/i18n/ja.json5 @@ -4284,9 +4284,9 @@ // TODO New key - Add a translation "mydspace.status.mydspaceValidation": "Validation", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // TODO New key - Add a translation - "mydspace.status.mydspaceWaitingController": "Waiting for controller", + "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // "mydspace.status.mydspaceWorkflow": "Workflow", // TODO New key - Add a translation diff --git a/src/assets/i18n/kk.json5 b/src/assets/i18n/kk.json5 index d23dc23c475..dbaa8078e2a 100644 --- a/src/assets/i18n/kk.json5 +++ b/src/assets/i18n/kk.json5 @@ -4145,7 +4145,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Валидация", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Контроллерді күтуде", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/lv.json5 b/src/assets/i18n/lv.json5 index 81e2383a1f8..3ceaac7fea9 100644 --- a/src/assets/i18n/lv.json5 +++ b/src/assets/i18n/lv.json5 @@ -3498,7 +3498,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validācija", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Gaida kontrolieri", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/nl.json5 b/src/assets/i18n/nl.json5 index 280a87b96fe..fc52543c383 100644 --- a/src/assets/i18n/nl.json5 +++ b/src/assets/i18n/nl.json5 @@ -3770,7 +3770,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validatie", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Wachten op controlleur", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/pt-BR.json5 b/src/assets/i18n/pt-BR.json5 index ce35f1ec055..5061c5a0e99 100644 --- a/src/assets/i18n/pt-BR.json5 +++ b/src/assets/i18n/pt-BR.json5 @@ -4533,7 +4533,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validação", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Esperando pelo controlador", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/pt-PT.json5 b/src/assets/i18n/pt-PT.json5 index faa027705e0..328cb208102 100644 --- a/src/assets/i18n/pt-PT.json5 +++ b/src/assets/i18n/pt-PT.json5 @@ -4478,7 +4478,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Em validação", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Aguarda validador", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/sv.json5 b/src/assets/i18n/sv.json5 index 4e3576ccfc0..d2fe72536c7 100644 --- a/src/assets/i18n/sv.json5 +++ b/src/assets/i18n/sv.json5 @@ -3940,7 +3940,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Validering", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Väntar på kontrollant", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/sw.json5 b/src/assets/i18n/sw.json5 index a470ee4b587..d2d663cdc53 100644 --- a/src/assets/i18n/sw.json5 +++ b/src/assets/i18n/sw.json5 @@ -4284,9 +4284,9 @@ // TODO New key - Add a translation "mydspace.status.mydspaceValidation": "Validation", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // TODO New key - Add a translation - "mydspace.status.mydspaceWaitingController": "Waiting for controller", + "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", // "mydspace.status.mydspaceWorkflow": "Workflow", // TODO New key - Add a translation diff --git a/src/assets/i18n/tr.json5 b/src/assets/i18n/tr.json5 index 153eaa1281a..75bc5c9a362 100644 --- a/src/assets/i18n/tr.json5 +++ b/src/assets/i18n/tr.json5 @@ -3263,7 +3263,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Doğrulama", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Kontrolör bekleniyor", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/i18n/uk.json5 b/src/assets/i18n/uk.json5 index 7df55fa2369..fae770b6bd3 100644 --- a/src/assets/i18n/uk.json5 +++ b/src/assets/i18n/uk.json5 @@ -3389,7 +3389,7 @@ // "mydspace.status.mydspaceValidation": "Validation", "mydspace.status.mydspaceValidation": "Перевірка", - // "mydspace.status.mydspaceWaitingController": "Waiting for controller", + // "mydspace.status.mydspaceWaitingController": "Waiting for reviewer", "mydspace.status.mydspaceWaitingController": "Чекаємо контролера", // "mydspace.status.mydspaceWorkflow": "Workflow", diff --git a/src/assets/images/ror-icon.svg b/src/assets/images/ror-icon.svg new file mode 100644 index 00000000000..24735df519b --- /dev/null +++ b/src/assets/images/ror-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 5a474ae561a..6c4b99cb0f4 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -14,6 +14,7 @@ import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; +import { SuggestionConfig } from './suggestion-config.interfaces'; import { BundleConfig } from './bundle-config.interface'; import { ActuatorsConfig } from './actuators.config'; import { InfoConfig } from './info-config.interface'; @@ -22,7 +23,7 @@ import { HomeConfig } from './homepage-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; -import {QualityAssuranceConfig} from './quality-assurance.config'; +import { QualityAssuranceConfig } from './quality-assurance.config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -43,6 +44,7 @@ interface AppConfig extends Config { collection: CollectionPageConfig; themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; + suggestion: SuggestionConfig[]; bundle: BundleConfig; actuators: ActuatorsConfig info: InfoConfig; diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index fc03038fdad..3b3eb00ddbf 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -14,6 +14,7 @@ import { ServerConfig } from './server-config.interface'; import { SubmissionConfig } from './submission-config.interface'; import { ThemeConfig } from './theme.config'; import { UIServerConfig } from './ui-server-config.interface'; +import {SuggestionConfig} from './suggestion-config.interfaces'; import { BundleConfig } from './bundle-config.interface'; import { ActuatorsConfig } from './actuators.config'; import { InfoConfig } from './info-config.interface'; @@ -297,6 +298,17 @@ export class DefaultAppConfig implements AppConfig { } }; + suggestion: SuggestionConfig[] = [ + // { + // // Use this configuration to map a suggestion import to a specific collection based on the suggestion type. + // source: 'suggestionSource', + // collectionId: 'collectionUUID' + // } + // This is used as a default fallback in case there aren't collections where to import the suggestion + // If not mapped the user will be allowed to import the suggestions only in the provided options, shown clicking the button "Approve and import" + // If not mapped and no options available for import the user won't be able to import the suggestions. + ]; + // Theme Config themes: ThemeConfig[] = [ // Add additional themes here. In the case where multiple themes match a route, the first one diff --git a/src/config/suggestion-config.interfaces.ts b/src/config/suggestion-config.interfaces.ts new file mode 100644 index 00000000000..afd3a38c581 --- /dev/null +++ b/src/config/suggestion-config.interfaces.ts @@ -0,0 +1,6 @@ +import { Config } from './config.interface'; + +export interface SuggestionConfig extends Config { + source: string; + collectionId: string; +} diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 8b906764620..afc4082dde3 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -319,5 +319,7 @@ export const environment: BuildConfig = { vocabulary: 'srsc', enabled: true } - ] + ], + + suggestion: [] }; diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 860efc585d6..7d2f80d451b 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -106,6 +106,8 @@ --ds-comcol-logo-max-width: 500px; --ds-comcol-logo-max-height: 500px; + --ds-item-page-img-field-default-inline-height: 24px; + --ds-process-overview-table-nb-processes-badge-size: 0.5em; --ds-process-overview-table-id-column-width: 120px; --ds-process-overview-table-name-column-width: auto; diff --git a/src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts deleted file mode 100644 index 9fcf7733504..00000000000 --- a/src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { BrowseByDatePageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-date-page/browse-by-date-page.component'; - -@Component({ - selector: 'ds-browse-by-date-page', - // styleUrls: ['./browse-by-date-page.component.scss'], - styleUrls: ['../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss'], - // templateUrl: './browse-by-date-page.component.html' - templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html' -}) - -/** - * Component for determining what Browse-By component to use depending on the metadata (browse ID) provided - */ - -export class BrowseByDatePageComponent extends BaseComponent { -} diff --git a/src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss b/src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.html similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss rename to src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.html diff --git a/src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.html b/src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.html rename to src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.scss diff --git a/src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.ts b/src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.ts new file mode 100644 index 00000000000..f37727769e4 --- /dev/null +++ b/src/themes/custom/app/browse-by/browse-by-date/browse-by-date.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { BrowseByDateComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-date/browse-by-date.component'; +import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type'; +import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator'; +import { Context } from '../../../../../app/core/shared/context.model'; + +@Component({ + selector: 'ds-browse-by-date', + // styleUrls: ['./browse-by-date.component.scss'], + styleUrls: ['../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.scss'], + // templateUrl: './browse-by-date.component.html', + templateUrl: '../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.html', +}) +@rendersBrowseBy(BrowseByDataType.Date, Context.Any, 'custom') +export class BrowseByDateComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts deleted file mode 100644 index 9434ca936dc..00000000000 --- a/src/themes/custom/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { BrowseByMetadataPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component'; - -@Component({ - selector: 'ds-browse-by-metadata-page', - // styleUrls: ['./browse-by-metadata-page.component.scss'], - styleUrls: ['../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss'], - // templateUrl: './browse-by-metadata-page.component.html' - templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html' -}) - -/** - * Component for determining what Browse-By component to use depending on the metadata (browse ID) provided - */ - -export class BrowseByMetadataPageComponent extends BaseComponent { -} diff --git a/src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.scss b/src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.html similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.scss rename to src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.html diff --git a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss similarity index 100% rename from src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html rename to src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss diff --git a/src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts b/src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts new file mode 100644 index 00000000000..765167b5914 --- /dev/null +++ b/src/themes/custom/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { BrowseByMetadataComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component'; +import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type'; +import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator'; +import { Context } from '../../../../../app/core/shared/context.model'; + +@Component({ + selector: 'ds-browse-by-metadata', + // styleUrls: ['./browse-by-metadata.component.scss'], + styleUrls: ['../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.scss'], + // templateUrl: './browse-by-metadata.component.html', + templateUrl: '../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.html', +}) +@rendersBrowseBy(BrowseByDataType.Metadata, Context.Any, 'custom') +export class BrowseByMetadataComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts deleted file mode 100644 index e65997eaf4c..00000000000 --- a/src/themes/custom/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core'; -import { BrowseBySwitcherComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-switcher/browse-by-switcher.component'; - -@Component({ - selector: 'ds-browse-by-switcher', - // styleUrls: ['./browse-by-switcher.component.scss'], - // templateUrl: './browse-by-switcher.component.html' - templateUrl: '../../../../../app/browse-by/browse-by-switcher/browse-by-switcher.component.html' -}) - -/** - * Component for determining what Browse-By component to use depending on the metadata (browse ID) provided - */ -export class BrowseBySwitcherComponent extends BaseComponent {} - diff --git a/src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts b/src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts deleted file mode 100644 index 34d80a0cb8a..00000000000 --- a/src/themes/custom/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core'; -import { BrowseByTaxonomyPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component'; - -@Component({ - selector: 'ds-browse-by-taxonomy-page', - // templateUrl: './browse-by-taxonomy-page.component.html', - templateUrl: '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html', - // styleUrls: ['./browse-by-taxonomy-page.component.scss'], - styleUrls: ['../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss'], -}) -/** - * Component for browsing items by metadata in a hierarchical controlled vocabulary - */ -export class BrowseByTaxonomyPageComponent extends BaseComponent { -} diff --git a/src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.html b/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html similarity index 100% rename from src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.html rename to src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html diff --git a/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss b/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts b/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts new file mode 100644 index 00000000000..800b23a8169 --- /dev/null +++ b/src/themes/custom/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { BrowseByTaxonomyComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component'; +import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type'; +import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator'; +import { Context } from '../../../../../app/core/shared/context.model'; + +@Component({ + selector: 'ds-browse-by-taxonomy', + // templateUrl: './browse-by-taxonomy.component.html', + templateUrl: '../../../../../app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html', + // styleUrls: ['./browse-by-taxonomy.component.scss'], + styleUrls: ['../../../../../app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss'], +}) +@rendersBrowseBy(BrowseByDataType.Hierarchy, Context.Any, 'custom') +export class BrowseByTaxonomyComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts deleted file mode 100644 index ed96a81110b..00000000000 --- a/src/themes/custom/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { BrowseByTitlePageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-title-page/browse-by-title-page.component'; - -@Component({ - selector: 'ds-browse-by-title-page', - // styleUrls: ['./browse-by-title-page.component.scss'], - styleUrls: ['../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss'], - // templateUrl: './browse-by-title-page.component.html' - templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html' -}) - -/** - * Component for determining what Browse-By component to use depending on the metadata (browse ID) provided - */ - -export class BrowseByTitlePageComponent extends BaseComponent { -} diff --git a/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.html b/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.scss b/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.ts b/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.ts new file mode 100644 index 00000000000..9bb328670d7 --- /dev/null +++ b/src/themes/custom/app/browse-by/browse-by-title/browse-by-title.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { BrowseByTitleComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-title/browse-by-title.component'; +import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type'; +import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator'; +import { Context } from '../../../../../app/core/shared/context.model'; + +@Component({ + selector: 'ds-browse-by-title', + // styleUrls: ['./browse-by-title.component.scss'], + styleUrls: ['../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.scss'], + // templateUrl: './browse-by-title.component.html', + templateUrl: '../../../../../app/browse-by/browse-by-metadata/browse-by-metadata.component.html', +}) +@rendersBrowseBy(BrowseByDataType.Title, Context.Any, 'custom') +export class BrowseByTitleComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.scss b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts new file mode 100644 index 00000000000..a3f51bf9162 --- /dev/null +++ b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { CommunityPageSubCollectionListComponent as BaseComponent } from '../../../../../../../app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component'; + +@Component({ + selector: 'ds-community-page-sub-collection-list', + // styleUrls: ['./community-page-sub-collection-list.component.scss'], + styleUrls: ['../../../../../app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.scss'], + // templateUrl: './community-page-sub-collection-list.component.html', + templateUrl: '../../../../../app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html', +}) +export class CommunityPageSubCollectionListComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.scss b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts new file mode 100644 index 00000000000..68af7dddd01 --- /dev/null +++ b/src/themes/custom/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { CommunityPageSubCommunityListComponent as BaseComponent } from '../../../../../../../app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component'; + +@Component({ + selector: 'ds-community-page-sub-community-list', + // styleUrls: ['./community-page-sub-community-list.component.scss'], + styleUrls: ['../../../../../../../app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.scss'], + // templateUrl: './community-page-sub-community-list.component.html', + templateUrl: '../../../../../../../app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html', +}) +export class CommunityPageSubCommunityListComponent extends BaseComponent { +} diff --git a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts deleted file mode 100644 index a228fe1071e..00000000000 --- a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; -import { CommunityPageSubCollectionListComponent as BaseComponent } - from '../../../../../app/community-page/sub-collection-list/community-page-sub-collection-list.component'; - -@Component({ - selector: 'ds-community-page-sub-collection-list', - // styleUrls: ['./community-page-sub-collection-list.component.scss'], - styleUrls: ['../../../../../app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss'], - // templateUrl: './community-page-sub-collection-list.component.html', - templateUrl: '../../../../../app/community-page/sub-collection-list/community-page-sub-collection-list.component.html' -}) -export class CommunityPageSubCollectionListComponent extends BaseComponent {} diff --git a/src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.ts deleted file mode 100644 index 1db265a844b..00000000000 --- a/src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; -import { CommunityPageSubCommunityListComponent as BaseComponent } - from '../../../../../app/community-page/sub-community-list/community-page-sub-community-list.component'; - -@Component({ - selector: 'ds-community-page-sub-community-list', - // styleUrls: ['./community-page-sub-community-list.component.scss'], - styleUrls: ['../../../../../app/community-page/sub-community-list/community-page-sub-community-list.component.scss'], - // templateUrl: './community-page-sub-community-list.component.html', - templateUrl: '../../../../../app/community-page/sub-community-list/community-page-sub-community-list.component.html' -}) -export class CommunityPageSubCommunityListComponent extends BaseComponent {} diff --git a/src/themes/custom/eager-theme.module.ts b/src/themes/custom/eager-theme.module.ts index 31047e239ac..8889c0c3df1 100644 --- a/src/themes/custom/eager-theme.module.ts +++ b/src/themes/custom/eager-theme.module.ts @@ -102,21 +102,24 @@ const DECLARATIONS = [ ]; @NgModule({ - imports: [ - CommonModule, - SharedModule, - RootModule, - NavbarModule, - SharedBrowseByModule, - ResultsBackButtonModule, - ItemPageModule, - ItemSharedModule, - DsoPageModule, - ], - declarations: DECLARATIONS, - providers: [ - ...ENTRY_COMPONENTS.map((component) => ({provide: component})) - ], + imports: [ + CommonModule, + SharedModule, + RootModule, + NavbarModule, + SharedBrowseByModule, + ResultsBackButtonModule, + ItemPageModule, + ItemSharedModule, + DsoPageModule, + ], + declarations: DECLARATIONS, + providers: [ + ...ENTRY_COMPONENTS.map((component) => ({ provide: component })) + ], + exports: [ + ItemSearchResultListElementComponent + ] }) /** * This module is included in the main bundle that gets downloaded at first page load. So it should diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 73400e78806..d2bac663b4f 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -46,7 +46,6 @@ import { RootModule } from '../../app/root.module'; import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component'; import { HomePageComponent } from './app/home-page/home-page.component'; import { RootComponent } from './app/root/root.component'; -import { BrowseBySwitcherComponent } from './app/browse-by/browse-by-switcher/browse-by-switcher.component'; import { CommunityListPageComponent } from './app/community-list-page/community-list-page.component'; import { SearchPageComponent } from './app/search-page/search-page.component'; import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component'; @@ -94,14 +93,14 @@ import { SearchResultsComponent } from './app/shared/search/search-results/searc import { AdminSidebarComponent } from './app/admin/admin-sidebar/admin-sidebar.component'; import { ComcolPageBrowseByComponent } from './app/shared/comcol-page-browse-by/comcol-page-browse-by.component'; import { SearchSettingsComponent } from './app/shared/search/search-settings/search-settings.component'; -import { CommunityPageSubCommunityListComponent } from './app/community-page/sub-community-list/community-page-sub-community-list.component'; -import { CommunityPageSubCollectionListComponent } from './app/community-page/sub-collection-list/community-page-sub-collection-list.component'; +import { CommunityPageSubCommunityListComponent } from './app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component'; +import { CommunityPageSubCollectionListComponent } from './app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component'; import { ObjectListComponent } from './app/shared/object-list/object-list.component'; -import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component'; -import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component'; -import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component'; -import { BrowseByTaxonomyPageComponent } from './app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component'; +import { BrowseByMetadataComponent } from './app/browse-by/browse-by-metadata/browse-by-metadata.component'; +import { BrowseByDateComponent } from './app/browse-by/browse-by-date/browse-by-date.component'; +import { BrowseByTitleComponent } from './app/browse-by/browse-by-title/browse-by-title.component'; +import { BrowseByTaxonomyComponent } from './app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component'; import { ExternalSourceEntryImportModalComponent } from './app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; @@ -156,15 +155,16 @@ import { ItemStatusComponent } from './app/item-page/edit-item-page/item-status/ import { EditBitstreamPageComponent } from './app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component'; import { FormModule } from '../../app/shared/form/form.module'; import { RequestCopyModule } from 'src/app/request-copy/request-copy.module'; +import { NotificationsModule } from '../../app/notifications/notifications.module'; import {UserMenuComponent} from './app/shared/auth-nav-menu/user-menu/user-menu.component'; import { BrowseByComponent } from './app/shared/browse-by/browse-by.component'; import { RegisterEmailFormComponent } from './app/register-email-form/register-email-form.component'; + const DECLARATIONS = [ FileSectionComponent, HomePageComponent, RootComponent, - BrowseBySwitcherComponent, CommunityListPageComponent, SearchPageComponent, ConfigurationSearchPageComponent, @@ -211,10 +211,10 @@ const DECLARATIONS = [ SearchSettingsComponent, ComcolPageBrowseByComponent, ObjectListComponent, - BrowseByMetadataPageComponent, - BrowseByDatePageComponent, - BrowseByTitlePageComponent, - BrowseByTaxonomyPageComponent, + BrowseByMetadataComponent, + BrowseByDateComponent, + BrowseByTitleComponent, + BrowseByTaxonomyComponent, ExternalSourceEntryImportModalComponent, SearchFiltersComponent, SearchSidebarComponent, @@ -305,11 +305,9 @@ const DECLARATIONS = [ NgxGalleryModule, FormModule, RequestCopyModule, + NotificationsModule ], declarations: DECLARATIONS, - exports: [ - CommunityPageSubCollectionListComponent - ] }) /** diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss index 516eff9f7e9..8512514a405 100644 --- a/src/themes/dspace/styles/_theme_css_variable_overrides.scss +++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss @@ -7,5 +7,7 @@ --ds-home-news-link-color: #{$green}; --ds-home-news-link-hover-color: #{darken($green, 15%)}; --ds-header-navbar-border-bottom-color: #{$green}; + --ds-item-page-img-field-default-inline-height: 24px; + --ds-item-page-img-field-ror-inline-height: var(--ds-item-page-img-field-default-inline-height); } diff --git a/yarn.lock b/yarn.lock index 6e04ab27b2a..786a832e56b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5859,9 +5859,9 @@ flatted@^3.1.0, flatted@^3.2.7: integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== for-each@^0.3.3: version "0.3.3"