diff --git a/config/config.example.yml b/config/config.example.yml index 69a9ffd320..4b9c1a27ac 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/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts new file mode 100644 index 0000000000..d7cbf846e4 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; + +/** + * Interface for the route parameters. + */ +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts +export interface NotificationsSuggestionTargetsPageParams { +======== +export interface AdminNotificationsPublicationClaimPageParams { +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts + pageId?: string; + pageSize?: number; + currentPage?: number; +} + +/** + * This class represents a resolver that retrieve the route data before the route is activated. + */ +@Injectable() +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts +export class NotificationsSuggestionTargetsPageResolver implements Resolve { +======== +export class AdminNotificationsPublicationClaimPageResolver implements Resolve { +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts + + /** + * 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 + */ +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): NotificationsSuggestionTargetsPageParams { +======== + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsPublicationClaimPageParams { +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts + 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 0000000000..b04e7132f1 --- /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/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.scss b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..6061bda063 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts @@ -0,0 +1,59 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts +import { NotificationsSuggestionTargetsPageComponent } from './notifications-suggestion-targets-page.component'; +======== +import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component'; +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; + +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts +describe('NotificationsSuggestionTargetsPageComponent', () => { + let component: NotificationsSuggestionTargetsPageComponent; + let fixture: ComponentFixture; +======== +describe('AdminNotificationsPublicationClaimPageComponent', () => { + let component: AdminNotificationsPublicationClaimPageComponent; + let fixture: ComponentFixture; +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot() + ], + declarations: [ +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts + NotificationsSuggestionTargetsPageComponent + ], + providers: [ + NotificationsSuggestionTargetsPageComponent +======== + AdminNotificationsPublicationClaimPageComponent + ], + providers: [ + AdminNotificationsPublicationClaimPageComponent +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts + fixture = TestBed.createComponent(NotificationsSuggestionTargetsPageComponent); +======== + fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent); +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts + 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 0000000000..2256a1bc36 --- /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 new file mode 100644 index 0000000000..f92a96d242 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000000..07a98aa080 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -0,0 +1,107 @@ +import { NgModule } from '@angular/core'; +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'; +import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service'; +import { AdminQualityAssuranceEventsPageResolver } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.resolver'; +import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component'; +import { AdminQualityAssuranceSourcePageResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service'; +import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; +import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service'; +import { + SourceDataResolver +} from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.resolver'; + +@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`, + component: AdminQualityAssuranceTopicsPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: QualityAssuranceBreadcrumbResolver, + openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver + }, + data: { + title: 'admin.quality-assurance.page.title', + breadcrumbKey: 'admin.quality-assurance', + showBreadcrumbsFluid: false + } + }, + { + canActivate: [ AuthenticatedGuard ], + path: `${QUALITY_ASSURANCE_EDIT_PATH}`, + component: AdminQualityAssuranceSourcePageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: I18nBreadcrumbResolver, + openaireQualityAssuranceSourceParams: AdminQualityAssuranceSourcePageResolver, + sourceData: SourceDataResolver + }, + data: { + title: 'admin.notifications.source.breadcrumbs', + breadcrumbKey: 'admin.notifications.source', + showBreadcrumbsFluid: false + } + }, + { + canActivate: [ AuthenticatedGuard ], + path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, + component: AdminQualityAssuranceEventsPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: QualityAssuranceBreadcrumbResolver, + openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver + }, + data: { + title: 'admin.notifications.event.page.title', + breadcrumbKey: 'admin.notifications.event', + showBreadcrumbsFluid: false + } + } + ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService, + AdminNotificationsPublicationClaimPageResolver, + SourceDataResolver, + AdminQualityAssuranceSourcePageResolver, + AdminQualityAssuranceTopicsPageResolver, + AdminQualityAssuranceEventsPageResolver, + AdminQualityAssuranceSourcePageResolver, + QualityAssuranceBreadcrumbResolver, + QualityAssuranceBreadcrumbService + ] +}) +/** + * Routing module for the Notifications section of the admin sidebar + */ +export class AdminNotificationsRoutingModule { + +} diff --git a/src/app/admin/admin-notifications/admin-notifications.module.ts b/src/app/admin/admin-notifications/admin-notifications.module.ts new file mode 100644 index 0000000000..d9efb4c288 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications.module.ts @@ -0,0 +1,33 @@ +import { CommonModule } from '@angular/common'; +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'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CoreModule.forRoot(), + AdminNotificationsRoutingModule, + NotificationsModule + ], + declarations: [ + AdminNotificationsPublicationClaimPageComponent, + AdminQualityAssuranceTopicsPageComponent, + AdminQualityAssuranceEventsPageComponent, + AdminQualityAssuranceSourcePageComponent + ], + entryComponents: [] +}) +/** + * This module handles all components related to the notifications pages + */ +export class AdminNotificationsModule { + +} 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 0000000000..b6f4142469 --- /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 0000000000..713500d6a7 --- /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 0000000000..11062210bb --- /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 0000000000..1a87fd7de6 --- /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/cache/builders/build-decorators.spec.ts b/src/app/core/cache/builders/build-decorators.spec.ts index 150a07f006..e4baaa4a5a 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/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index c7cd5b0a70..84b1686024 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -29,11 +29,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 { @@ -385,7 +385,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 3ffcd9adf2..d2e009f669 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 57884e537e..bc0c1fb613 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 e30c394a34..1f93671458 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 bd2a8d2929..66ae73405e 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 536d6d6e25..ff0b492945 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/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index 0a765de101..8393d71460 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 0000000000..426fa87eb6 --- /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 9f707a82da..715b2ee413 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 { RemoteData } from './remote-data'; -import { RestRequestMethod } from './rest-request-method'; 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 { 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 dd3f9eec94..0579b998b8 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 c1bc3563a3..cbddf1e6c3 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/notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts b/src/app/core/notifications/models/suggestion-objects.resource-type.ts similarity index 76% rename from src/app/core/notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts rename to src/app/core/notifications/models/suggestion-objects.resource-type.ts index 8f87027a8c..8f83d86376 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts +++ b/src/app/core/notifications/models/suggestion-objects.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../../shared/resource-type'; +import { ResourceType } from '../../shared/resource-type'; /** * The resource type for the Suggestion object diff --git a/src/app/core/notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts b/src/app/core/notifications/models/suggestion-source-object.resource-type.ts similarity index 77% rename from src/app/core/notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts rename to src/app/core/notifications/models/suggestion-source-object.resource-type.ts index 2e26fe4301..e319ed5109 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts +++ b/src/app/core/notifications/models/suggestion-source-object.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../../shared/resource-type'; +import { ResourceType } from '../../shared/resource-type'; /** * The resource type for the Suggestion Source object diff --git a/src/app/core/notifications/reciter-suggestions/models/suggestion-source.model.ts b/src/app/core/notifications/models/suggestion-source.model.ts similarity index 71% rename from src/app/core/notifications/reciter-suggestions/models/suggestion-source.model.ts rename to src/app/core/notifications/models/suggestion-source.model.ts index 007520800d..12d9d7e9d8 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion-source.model.ts +++ b/src/app/core/notifications/models/suggestion-source.model.ts @@ -1,11 +1,11 @@ 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'; +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 diff --git a/src/app/core/notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts b/src/app/core/notifications/models/suggestion-target-object.resource-type.ts similarity index 77% rename from src/app/core/notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts rename to src/app/core/notifications/models/suggestion-target-object.resource-type.ts index 71dd41912a..81b1b5c261 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts +++ b/src/app/core/notifications/models/suggestion-target-object.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../../shared/resource-type'; +import { ResourceType } from '../../shared/resource-type'; /** * The resource type for the Suggestion Target object diff --git a/src/app/core/notifications/reciter-suggestions/models/suggestion-target.model.ts b/src/app/core/notifications/models/suggestion-target.model.ts similarity index 75% rename from src/app/core/notifications/reciter-suggestions/models/suggestion-target.model.ts rename to src/app/core/notifications/models/suggestion-target.model.ts index 2afe170e77..99d9a8628a 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion-target.model.ts +++ b/src/app/core/notifications/models/suggestion-target.model.ts @@ -1,12 +1,12 @@ import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../../../cache/cacheable-object.model'; +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'; +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 diff --git a/src/app/core/notifications/reciter-suggestions/models/suggestion.model.ts b/src/app/core/notifications/models/suggestion.model.ts similarity index 74% rename from src/app/core/notifications/reciter-suggestions/models/suggestion.model.ts rename to src/app/core/notifications/models/suggestion.model.ts index c36d36794b..ad58b1cfe5 100644 --- a/src/app/core/notifications/reciter-suggestions/models/suggestion.model.ts +++ b/src/app/core/notifications/models/suggestion.model.ts @@ -1,13 +1,16 @@ 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'; +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; diff --git a/src/app/core/notifications/reciter-suggestions/target/suggestion-target-data.service.ts b/src/app/core/notifications/reciter-suggestions/target/suggestion-target-data.service.ts deleted file mode 100644 index ce5f131c1d..0000000000 --- a/src/app/core/notifications/reciter-suggestions/target/suggestion-target-data.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -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 searchBy: SearchData; - protected searchFindBySourceMethod = 'findBySource'; - protected searchFindByTargetMethod = 'findByTarget'; - protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; - - 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.searchBy = 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.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow); - } - /** - * Return a single Suggestion target. - * - * @param id The Suggestion Target 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 getTarget(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...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.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); - } - -} diff --git a/src/app/core/notifications/reciter-suggestions/source/suggestion-source-data.service.ts b/src/app/core/notifications/source/suggestion-source-data.service.ts similarity index 59% rename from src/app/core/notifications/reciter-suggestions/source/suggestion-source-data.service.ts rename to src/app/core/notifications/source/suggestion-source-data.service.ts index c3e142044e..f00a84c95b 100644 --- a/src/app/core/notifications/reciter-suggestions/source/suggestion-source-data.service.ts +++ b/src/app/core/notifications/source/suggestion-source-data.service.ts @@ -1,24 +1,27 @@ import { Injectable } from '@angular/core'; -import { dataService } from '../../../data/base/data-service.decorator'; +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 { IdentifiableDataService } from '../../data/base/identifiable-data.service'; import { SuggestionSource } from '../models/suggestion-source.model'; -import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; +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 { 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 { 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'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; +/** + * Service that retrieves Suggestion Source data + */ @Injectable() @dataService(SUGGESTION_SOURCE) export class SuggestionSourceDataService extends IdentifiableDataService { @@ -53,7 +56,7 @@ export class SuggestionSourceDataService extends IdentifiableDataService[]): Observable>> { - return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + return this.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -71,4 +74,23 @@ export class SuggestionSourceDataService extends IdentifiableDataService[]): 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/notifications/source/suggestions-source-data.service.spec.ts b/src/app/core/notifications/source/suggestions-source-data.service.spec.ts new file mode 100644 index 0000000000..28f34b863d --- /dev/null +++ b/src/app/core/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/notifications/suggestion-data.service.spec.ts b/src/app/core/notifications/suggestion-data.service.spec.ts new file mode 100644 index 0000000000..c0bc97ea12 --- /dev/null +++ b/src/app/core/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/notifications/reciter-suggestions/suggestions-data.service.ts b/src/app/core/notifications/suggestions-data.service.ts similarity index 76% rename from src/app/core/notifications/reciter-suggestions/suggestions-data.service.ts rename to src/app/core/notifications/suggestions-data.service.ts index 944a13e3a3..17b1482578 100644 --- a/src/app/core/notifications/reciter-suggestions/suggestions-data.service.ts +++ b/src/app/core/notifications/suggestions-data.service.ts @@ -5,39 +5,35 @@ 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 { DataService } from '../../data/data.service'; -import { ChangeAnalyzer } from '../../data/change-analyzer'; -import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; -import { RemoteData } from '../../data/remote-data'; +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 { 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 { 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 DataService implementation to delegate specific methods to. + * A private UpdateDataServiceImpl implementation to delegate specific methods to. */ -class SuggestionDataServiceImpl extends DataService { - /** - * The REST endpoint. - */ - protected linkPath = 'suggestions'; +export class SuggestionDataServiceImpl extends UpdateDataServiceImpl { /** * Initialize service variables @@ -49,6 +45,7 @@ class SuggestionDataServiceImpl extends DataService { * @param {NotificationsService} notificationsService * @param {HttpClient} http * @param {ChangeAnalyzer} comparator + * @param responseMsToLive */ constructor( protected requestService: RequestService, @@ -58,8 +55,10 @@ class SuggestionDataServiceImpl extends DataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: ChangeAnalyzer) { - super(); + protected comparator: ChangeAnalyzer, + protected responseMsToLive: number, + ) { + super('suggestions', requestService, rdbService, objectCache, halService, notificationsService, comparator ,responseMsToLive); } } @@ -70,24 +69,25 @@ class SuggestionDataServiceImpl extends DataService { @dataService(SUGGESTION) export class SuggestionsDataService { protected searchFindBySourceMethod = 'findBySource'; - protected searchFindByTargetMethod = 'findByTarget'; protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; /** - * A private DataService implementation to delegate specific methods to. + * A private UpdateDataServiceImpl implementation to delegate specific methods to. */ private suggestionsDataService: SuggestionDataServiceImpl; /** - * A private DataService implementation to delegate specific methods to. + * A private UpdateDataServiceImpl implementation to delegate specific methods to. */ private suggestionSourcesDataService: SuggestionSourceDataService; /** - * A private DataService implementation to delegate specific methods to. + * A private UpdateDataServiceImpl implementation to delegate specific methods to. */ private suggestionTargetsDataService: SuggestionTargetDataService; + private responseMsToLive = 10 * 1000; + /** * Initialize service variables * @param {RequestService} requestService @@ -111,13 +111,13 @@ export class SuggestionsDataService { protected comparatorSources: DefaultChangeAnalyzer, protected comparatorTargets: DefaultChangeAnalyzer, ) { - this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions); + 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 Target + * Return the list of Suggestion Sources * * @param options * Find list options object. @@ -168,7 +168,6 @@ export class SuggestionsDataService { ...linksToFollow: FollowLinkConfig[] ): Observable>> { options.searchParams = [new RequestParam('target', userId)]; - //return this.suggestionTargetsDataService.getTargetsByUser(this.searchFindByTargetMethod, options, ...linksToFollow); return this.suggestionTargetsDataService.getTargetsByUser(userId, options, ...linksToFollow); } @@ -193,14 +192,6 @@ export class SuggestionsDataService { return this.suggestionsDataService.delete(suggestionId); } - /** - * Used to fetch Suggestion notification for user - * @suggestionId - */ - public getSuggestion(suggestionId: string, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.suggestionsDataService.findById(suggestionId, true, true, ...linksToFollow); - } - /** * Return the list of Suggestion for a given target and source * @@ -226,7 +217,7 @@ export class SuggestionsDataService { new RequestParam('source', source) ]; - return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, true, true, ...linksToFollow); + return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, false, true, ...linksToFollow); } /** diff --git a/src/app/core/notifications/target/suggestion-target-data.service.ts b/src/app/core/notifications/target/suggestion-target-data.service.ts new file mode 100644 index 0000000000..a2f1507b10 --- /dev/null +++ b/src/app/core/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/notifications/target/suggestions-target-data.service.spec.ts b/src/app/core/notifications/target/suggestions-target-data.service.spec.ts new file mode 100644 index 0000000000..9207603a5a --- /dev/null +++ b/src/app/core/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/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index 80ba200d38..bf232cb141 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/submission/workflowitem-data.service.spec.ts b/src/app/core/submission/workflowitem-data.service.spec.ts index 3f6ec54fda..64ffbe5718 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 2a3a3d343d..25a849baa2 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'; @@ -19,7 +19,13 @@ import { Item } from '../shared/item.model'; import { WorkspaceItem } from './models/workspaceitem.model'; 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; @@ -67,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(); @@ -83,20 +86,23 @@ describe('WorkspaceitemDataService test', () => { function initTestService() { hrefOnlyDataService = getMockHrefOnlyDataService(); return new WorkspaceitemDataService( - comparator, + comparatorEntry, halService, http, notificationsService, requestService, rdbService, objectCache, - store, + store ); } describe('composition', () => { - const initService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null); - 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('', () => { @@ -105,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; @@ -121,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), 'getIDHref').and.callThrough(); }); afterEach(() => { @@ -135,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).toHaveBeenCalled(); + 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', () => { @@ -151,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 f430dd43ea..91a9115c52 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -23,16 +23,19 @@ 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'; /** * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() @dataService(WorkspaceItem.type) -export class WorkspaceitemDataService extends IdentifiableDataService { +export class WorkspaceitemDataService extends IdentifiableDataService implements DeleteData, SearchData{ protected linkPath = 'workspaceitems'; protected searchByItemLinkPath = 'item'; private deleteData: DeleteData; + private searchData: SearchData; constructor( protected comparator: DSOChangeAnalyzer, @@ -45,7 +48,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService) { super('workspaceitems', requestService, rdbService, objectCache, halService); 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); @@ -107,4 +110,34 @@ export class WorkspaceitemDataService extends IdentifiableDataService> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + /** + * 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?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } } 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 d44817be84..60b1dffa45 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/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 c79d19e267..52df841d3b 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 @@
+ + - + @@ -23,6 +23,17 @@ + + + + + {{value}} diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index fcf98b094d..29c20c6e2a 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -4,7 +4,7 @@ import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; import { hasValue } from '../../../shared/empty.util'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type'; -import { environment } from './../../../../environments/environment'; +import { ImageField } from '../../simple/field-components/specific-field/item-page-field.component'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component. @@ -56,6 +56,11 @@ export class MetadataValuesComponent implements OnChanges { @Input() browseDefinition?: BrowseDefinition; + /** + * Optional {@code ImageField} reference that represents an image to be displayed inline. + */ + @Input() img?: ImageField; + ngOnChanges(changes: SimpleChanges): void { this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown; } @@ -91,13 +96,4 @@ export class MetadataValuesComponent implements OnChanges { } return queryParams; } - - /** - * Checks if the given link value is an internal link. - * @param linkValue - The link value to check. - * @returns True if the link value starts with the base URL defined in the environment configuration, false otherwise. - */ - hasInternalLink(linkValue: string): boolean { - return linkValue.startsWith(environment.ui.baseUrl); - } } diff --git a/src/app/item-page/item-shared.module.ts b/src/app/item-page/item-shared.module.ts index 9c2bbba619..8b7243acde 100644 --- a/src/app/item-page/item-shared.module.ts +++ b/src/app/item-page/item-shared.module.ts @@ -1,21 +1,36 @@ -import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component'; +import { + RelatedEntitiesSearchComponent +} from './simple/related-entities/related-entities-search/related-entities-search.component'; import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { CommonModule, NgOptimizedImage } from '@angular/common'; import { SearchModule } from '../shared/search/search.module'; import { SharedModule } from '../shared/shared.module'; import { TranslateModule } from '@ngx-translate/core'; import { DYNAMIC_FORM_CONTROL_MAP_FN } from '@ng-dynamic-forms/core'; -import { dsDynamicFormControlMapFn } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; -import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component'; -import { ItemVersionsDeleteModalComponent } from './versions/item-versions-delete-modal/item-versions-delete-modal.component'; -import { ItemVersionsSummaryModalComponent } from './versions/item-versions-summary-modal/item-versions-summary-modal.component'; +import { + dsDynamicFormControlMapFn +} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; +import { + TabbedRelatedEntitiesSearchComponent +} from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component'; +import { + ItemVersionsDeleteModalComponent +} from './versions/item-versions-delete-modal/item-versions-delete-modal.component'; +import { + ItemVersionsSummaryModalComponent +} from './versions/item-versions-summary-modal/item-versions-summary-modal.component'; import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component'; -import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component'; -import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component'; +import { + GenericItemPageFieldComponent +} from './simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { + MetadataRepresentationListComponent +} from './simple/metadata-representation-list/metadata-representation-list.component'; import { RelatedItemsComponent } from './simple/related-items/related-items-component'; import { ThemedMetadataRepresentationListComponent } from './simple/metadata-representation-list/themed-metadata-representation-list.component'; +import { ItemPageImgFieldComponent } from './simple/field-components/specific-field/img/item-page-img-field.component'; const ENTRY_COMPONENTS = [ ItemVersionsDeleteModalComponent, @@ -32,6 +47,7 @@ const COMPONENTS = [ MetadataRepresentationListComponent, ThemedMetadataRepresentationListComponent, RelatedItemsComponent, + ItemPageImgFieldComponent, ]; @NgModule({ @@ -42,7 +58,8 @@ const COMPONENTS = [ CommonModule, SearchModule, SharedModule, - TranslateModule + TranslateModule, + NgOptimizedImage ], exports: [ ...COMPONENTS diff --git a/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.spec.ts new file mode 100644 index 0000000000..b96daa47ad --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ItemPageImgFieldComponent } from './item-page-img-field.component'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; +import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { environment } from '../../../../../../environments/environment'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; +import { GenericItemPageFieldComponent } from '../generic/generic-item-page-field.component'; +import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; +import { By } from '@angular/platform-browser'; +import { ImageField } from '../item-page-field.component'; + +let component: ItemPageImgFieldComponent; +let fixture: ComponentFixture; + +const mockField = 'organization.identifier.ror'; +const mockValue = 'http://ror.org/awesome-identifier'; +const mockLabel = 'ROR label'; +const mockUrlRegex = '(.*)ror.org'; +const mockImg = { + URI: './assets/images/ror-icon.svg', + alt: 'item.page.image.alt.ROR', + heightVar: '--ds-item-page-img-field-ror-inline-height' +} as ImageField; + +describe('ItemPageImgFieldComponent', () => { + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + })], + providers: [ + { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } + ], + declarations: [ItemPageImgFieldComponent, GenericItemPageFieldComponent, MetadataValuesComponent], + schemas: [NO_ERRORS_SCHEMA] + }) + .overrideComponent(GenericItemPageFieldComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }) + .compileComponents(); + + fixture = TestBed.createComponent(ItemPageImgFieldComponent); + component = fixture.componentInstance; + component.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue); + component.fields = [mockField]; + component.label = mockLabel; + component.urlRegex = mockUrlRegex; + component.img = mockImg; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display display img tag', () => { + const image = fixture.debugElement.query(By.css('img.link-logo')); + expect(image).not.toBeNull(); + }); + + it('should have right attributes', () => { + const image = fixture.debugElement.query(By.css('img.link-logo')); + expect(image.attributes.src).toEqual(mockImg.URI); + expect(image.attributes.alt).toEqual(mockImg.alt); + + const imageEl = image.nativeElement; + expect(imageEl.style.height).toContain(mockImg.heightVar); + }); + + it('should have the right value', () => { + const imageAnchor = fixture.debugElement.query(By.css('a.link-anchor')); + const anchorEl = imageAnchor.nativeElement; + expect(anchorEl.innerHTML).toContain(mockValue); + }); +}); diff --git a/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.ts b/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.ts new file mode 100644 index 0000000000..d442323b53 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/img/item-page-img-field.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; +import { ImageField, ItemPageFieldComponent } from '../item-page-field.component'; +import { Item } from '../../../../../core/shared/item.model'; + +@Component({ + selector: 'ds-item-page-img-field', + templateUrl: '../item-page-field.component.html' +}) +/** + * Component that renders an inline image for a given field. + * This component uses a given {@code ImageField} configuration to correctly render the img. + */ +export class ItemPageImgFieldComponent extends ItemPageFieldComponent { + + /** + * The item to display metadata for + */ + @Input() item: Item; + + /** + * Separator string between multiple values of the metadata fields defined + * @type {string} + */ + @Input() separator: string; + + /** + * Fields (schema.element.qualifier) used to render their values. + */ + @Input() fields: string[]; + + /** + * Label i18n key for the rendered metadata + */ + @Input() label: string; + + /** + * Image Configuration + */ + @Input() img: ImageField; + + /** + * Whether any valid HTTP(S) URL should be rendered as a link + */ + @Input() urlRegex?: string; + +} diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html index 91d40b0ad7..f45d4657a4 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html @@ -6,5 +6,6 @@ [enableMarkdown]="enableMarkdown" [urlRegex]="urlRegex" [browseDefinition]="browseDefinition|async" + [img]="img" >
diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts index fc526dabcc..57f49e3647 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -6,6 +6,25 @@ import { BrowseDefinition } from '../../../../core/shared/browse-definition.mode import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; import { getRemoteDataPayload } from '../../../../core/shared/operators'; +/** + * Interface that encapsulate Image configuration for this component. + */ +export interface ImageField { + /** + * URI that is used to retrieve the image. + */ + URI: string; + /** + * i18n Key that represents the alt text to display + */ + alt: string; + /** + * CSS variable that contains the height of the inline image. + */ + heightVar: string; +} + + /** * This component can be used to represent metadata on a simple item page. * It expects one input parameter of type Item to which the metadata belongs. @@ -51,6 +70,11 @@ export class ItemPageFieldComponent { */ urlRegex?: string; + /** + * Image Configuration + */ + img: ImageField; + /** * Return browse definition that matches any field used in this component if it is configured as a browse * link in dspace.cfg (webui.browse.link.) diff --git a/src/app/notifications/notifications-effects.ts b/src/app/notifications/notifications-effects.ts index c401cb5951..a73fd04fc0 100644 --- a/src/app/notifications/notifications-effects.ts +++ b/src/app/notifications/notifications-effects.ts @@ -1,6 +1,6 @@ import { QualityAssuranceSourceEffects } from './qa/source/quality-assurance-source.effects'; import { QualityAssuranceTopicsEffects } from './qa/topics/quality-assurance-topics.effects'; -import {SuggestionTargetsEffects} from './reciter-suggestions/suggestion-targets/suggestion-targets.effects'; +import { SuggestionTargetsEffects } from '../suggestion-notifications/suggestion-targets/suggestion-targets.effects'; export const notificationsEffects = [ QualityAssuranceTopicsEffects, diff --git a/src/app/notifications/notifications.module.ts b/src/app/notifications/notifications.module.ts index 20100b70f6..00c7582b2f 100644 --- a/src/app/notifications/notifications.module.ts +++ b/src/app/notifications/notifications.module.ts @@ -26,29 +26,30 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou import { QualityAssuranceSourceDataService } from '../core/notifications/qa/source/quality-assurance-source-data.service'; -import { SuggestionTargetsComponent } from './reciter-suggestions/suggestion-targets/suggestion-targets.component'; -import { SuggestionActionsComponent } from './reciter-suggestions/suggestion-actions/suggestion-actions.component'; +import { PublicationClaimComponent } from '../suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component'; +import { SuggestionActionsComponent } from '../suggestion-notifications/suggestion-actions/suggestion-actions.component'; import { SuggestionListElementComponent -} from './reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; +} from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component'; import { SuggestionEvidencesComponent -} from './reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; -import { SuggestionsPopupComponent } from './reciter-suggestions/suggestions-popup/suggestions-popup.component'; +} from '../suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; +import { SuggestionsPopupComponent } from '../suggestion-notifications/suggestions-popup/suggestions-popup.component'; import { SuggestionsNotificationComponent -} from './reciter-suggestions/suggestions-notification/suggestions-notification.component'; -import { SuggestionsService } from './reciter-suggestions/suggestions.service'; -import { SuggestionsDataService } from '../core/notifications/reciter-suggestions/suggestions-data.service'; +} from '../suggestion-notifications/suggestions-notification/suggestions-notification.component'; +import { SuggestionsService } from '../suggestion-notifications/suggestions.service'; +import { SuggestionsDataService } from '../core/suggestion-notifications/suggestions-data.service'; import { SuggestionSourceDataService -} from '../core/notifications/reciter-suggestions/source/suggestion-source-data.service'; +} from '../core/suggestion-notifications/source/suggestion-source-data.service'; import { SuggestionTargetDataService -} from '../core/notifications/reciter-suggestions/target/suggestion-target-data.service'; +} from '../core/suggestion-notifications/target/suggestion-target-data.service'; import { SuggestionTargetsStateService -} from './reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; +} from '../suggestion-notifications/suggestion-targets/suggestion-targets.state.service'; + const MODULES = [ CommonModule, @@ -64,7 +65,7 @@ const COMPONENTS = [ QualityAssuranceTopicsComponent, QualityAssuranceEventsComponent, QualityAssuranceSourceComponent, - SuggestionTargetsComponent, + PublicationClaimComponent, SuggestionActionsComponent, SuggestionListElementComponent, SuggestionEvidencesComponent, diff --git a/src/app/notifications/notifications.reducer.ts b/src/app/notifications/notifications.reducer.ts index 5c42e412f5..cced6755fa 100644 --- a/src/app/notifications/notifications.reducer.ts +++ b/src/app/notifications/notifications.reducer.ts @@ -1,7 +1,7 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; import { qualityAssuranceSourceReducer, QualityAssuranceSourceState } from './qa/source/quality-assurance-source.reducer'; import { qualityAssuranceTopicsReducer, QualityAssuranceTopicState, } from './qa/topics/quality-assurance-topics.reducer'; -import { SuggestionTargetsReducer, SuggestionTargetState } from './reciter-suggestions/suggestion-targets/suggestion-targets.reducer'; +import { SuggestionTargetsReducer, SuggestionTargetState } from '../suggestion-notifications/suggestion-targets/suggestion-targets.reducer'; /** * The OpenAIRE State diff --git a/src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.html b/src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.html deleted file mode 100644 index 577aa496b3..0000000000 --- a/src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - -
-
-
-
-
-
diff --git a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.html b/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.html deleted file mode 100644 index 8b13789179..0000000000 --- a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts b/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts deleted file mode 100644 index 820df205b3..0000000000 --- a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { SuggestionsService } from '../suggestions.service'; -import { takeUntil } from 'rxjs/operators'; -import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; -import { isNotEmpty } from '../../../shared/empty.util'; -import { combineLatest, Subject } from 'rxjs'; - -@Component({ - selector: 'ds-suggestions-popup', - templateUrl: './suggestions-popup.component.html', - styleUrls: ['./suggestions-popup.component.scss'] -}) -export class SuggestionsPopupComponent implements OnInit, OnDestroy { - - labelPrefix = 'mydspace.'; - - subscription; - - constructor( - private translateService: TranslateService, - private reciterSuggestionStateService: SuggestionTargetsStateService, - private notificationsService: NotificationsService, - private suggestionsService: SuggestionsService - ) { } - - ngOnInit() { - this.initializePopup(); - } - - public initializePopup() { - this.reciterSuggestionStateService.dispatchRefreshUserSuggestionsAction(); - const notifier = new Subject(); - this.subscription = combineLatest([ - this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(), - this.reciterSuggestionStateService.hasUserVisitedSuggestions() - ]).pipe(takeUntil(notifier)).subscribe(([suggestions, visited]) => { - if (isNotEmpty(suggestions)) { - if (!visited) { - suggestions.forEach((suggestionTarget: SuggestionTarget) => this.showNotificationForNewSuggestions(suggestionTarget)); - this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction(); - notifier.next(null); - notifier.complete(); - } - } - }); - } - - /** - * Show a notification to user for a new suggestions detected - * @param suggestionTarget - * @private - */ - private showNotificationForNewSuggestions(suggestionTarget: SuggestionTarget): void { - const content = this.translateService.instant(this.labelPrefix + 'notification.suggestion', - this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget)); - this.notificationsService.success('', content, {timeOut:0}, true); - } - - ngOnDestroy() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - -} diff --git a/src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.html b/src/app/notifications/suggestion-actions/suggestion-actions.component.html similarity index 69% rename from src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.html rename to src/app/notifications/suggestion-actions/suggestion-actions.component.html index 7ec3e61395..2a46191dee 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.html +++ b/src/app/notifications/suggestion-actions/suggestion-actions.component.html @@ -19,10 +19,11 @@ - - + diff --git a/src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss b/src/app/notifications/suggestion-actions/suggestion-actions.component.scss similarity index 100% rename from src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss rename to src/app/notifications/suggestion-actions/suggestion-actions.component.scss diff --git a/src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts b/src/app/notifications/suggestion-actions/suggestion-actions.component.ts similarity index 66% rename from src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts rename to src/app/notifications/suggestion-actions/suggestion-actions.component.ts index ea2e4cfe33..6af1e2c67a 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts +++ b/src/app/notifications/suggestion-actions/suggestion-actions.component.ts @@ -1,12 +1,19 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { ItemType } from '../../core/shared/item-relationships/item-type.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts import { Suggestion } from '../../../core/notifications/reciter-suggestions/models/suggestion.model'; +======== +import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model'; +>>>>>>>> main:src/app/notifications/suggestion-actions/suggestion-actions.component.ts import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-list-element.component'; -import { Collection } from '../../../core/shared/collection.model'; +import { Collection } from '../../core/shared/collection.model'; import { take } from 'rxjs/operators'; -import { CreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +/** + * Show and trigger the actions to submit for a suggestion + */ @Component({ selector: 'ds-suggestion-actions', styleUrls: [ './suggestion-actions.component.scss' ], @@ -27,7 +34,7 @@ export class SuggestionActionsComponent { /** * The component is used to Delete suggestion */ - @Output() notMineClicked = new EventEmitter(); + @Output() ignoreSuggestionClicked = new EventEmitter(); /** * The component is used to approve & import @@ -71,8 +78,8 @@ export class SuggestionActionsComponent { /** * Delete the suggestion */ - notMine() { - this.notMineClicked.emit(this.isBulk ? undefined : this.object.id); + ignoreSuggestion() { + this.ignoreSuggestionClicked.emit(this.isBulk ? undefined : this.object.id); } /** @@ -82,11 +89,11 @@ export class SuggestionActionsComponent { this.seeEvidences.emit(!this.seeEvidence); } - notMineLabel(): string { - return this.isBulk ? 'reciter.suggestion.notMine.bulk' : 'reciter.suggestion.notMine' ; + ignoreSuggestionLabel(): string { + return this.isBulk ? 'suggestion.ignoreSuggestion.bulk' : 'suggestion.ignoreSuggestion' ; } approveAndImportLabel(): string { - return this.isBulk ? 'reciter.suggestion.approveAndImport.bulk' : 'reciter.suggestion.approveAndImport'; + return this.isBulk ? 'suggestion.approveAndImport.bulk' : 'suggestion.approveAndImport'; } } diff --git a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html b/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html similarity index 66% rename from src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html rename to src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html index 5ad4f0a978..e23c244eb4 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html +++ b/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html @@ -3,9 +3,9 @@ - - - + + + diff --git a/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss b/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts b/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts similarity index 50% rename from src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts rename to src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts index 3f1e3d19b8..2346c86257 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts +++ b/src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts @@ -1,7 +1,15 @@ import { Component, Input } from '@angular/core'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts import { fadeIn } from '../../../../shared/animations/fade'; import { SuggestionEvidences } from '../../../../core/notifications/reciter-suggestions/models/suggestion.model'; +======== +import { fadeIn } from '../../../shared/animations/fade'; +import { SuggestionEvidences } from '../../../core/suggestion-notifications/models/suggestion.model'; +>>>>>>>> main:src/app/notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts +/** + * Show suggestion evidences such as score (authorScore, dateScore) + */ @Component({ selector: 'ds-suggestion-evidences', styleUrls: [ './suggestion-evidences.component.scss' ], diff --git a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.html similarity index 85% rename from src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html rename to src/app/notifications/suggestion-list-element/suggestion-list-element.component.html index f37d595c45..ef27876f2c 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html +++ b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.html @@ -4,13 +4,14 @@
+ [checked]="isSelected" (change)="changeSelected($event)" + [attr.aria-label]="object.display"/>
-
{{'reciter.suggestion.totalScore' | translate}}
+
{{'suggestion.totalScore' | translate}}
{{ object.score }}
@@ -19,6 +20,7 @@ @@ -29,7 +31,7 @@ [isCollectionFixed]="isCollectionFixed" (approveAndImport)="onApproveAndImport($event)" (seeEvidences)="onSeeEvidences($event)" - (notMineClicked)="onNotMine($event)" + (ignoreSuggestionClicked)="onIgnoreSuggestion($event)" > diff --git a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.scss similarity index 100% rename from src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss rename to src/app/notifications/suggestion-list-element/suggestion-list-element.component.scss diff --git a/src/app/notifications/suggestion-list-element/suggestion-list-element.component.spec.ts b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.spec.ts new file mode 100644 index 0000000000..61d1a19996 --- /dev/null +++ b/src/app/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/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.ts similarity index 73% rename from src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts rename to src/app/notifications/suggestion-list-element/suggestion-list-element.component.ts index 08604d9daf..76d1314417 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts +++ b/src/app/notifications/suggestion-list-element/suggestion-list-element.component.ts @@ -2,16 +2,29 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts import { fadeIn } from '../../../shared/animations/fade'; import { Suggestion } from '../../../core/notifications/reciter-suggestions/models/suggestion.model'; import { Item } from '../../../core/shared/item.model'; import { isNotEmpty } from '../../../shared/empty.util'; +======== +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'; +>>>>>>>> main:src/app/notifications/suggestion-list-element/suggestion-list-element.component.ts +/** + * 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'], @@ -33,7 +46,7 @@ export class SuggestionListElementComponent implements OnInit { /** * The component is used to Delete suggestion */ - @Output() notMineClicked = new EventEmitter(); + @Output() ignoreSuggestionClicked = new EventEmitter(); /** * The component is used to approve & import @@ -69,8 +82,8 @@ export class SuggestionListElementComponent implements OnInit { /** * Delete the suggestion */ - onNotMine(suggestionId: string) { - this.notMineClicked.emit(suggestionId); + onIgnoreSuggestion(suggestionId: string) { + this.ignoreSuggestionClicked.emit(suggestionId); } /** diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.html b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.html similarity index 65% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.html rename to src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.html index 280f574ec4..f2ab1e5b65 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.html +++ b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.html @@ -1,9 +1,9 @@
- +

{{'suggestion.title'| translate}}

- + - +
{{'reciter.suggestion.evidence.score' | translate}}{{'reciter.suggestion.evidence.type' | translate}}{{'reciter.suggestion.evidence.notes' | translate}}{{'suggestion.evidence.score' | translate}}{{'suggestion.evidence.type' | translate}}{{'suggestion.evidence.notes' | translate}}
- - + +
{{'reciter.suggestion.table.name' | translate}}{{'reciter.suggestion.table.actions' | translate}}{{'suggestion.table.name' | translate}}{{'suggestion.table.actions' | translate}}
- {{targetElement.display}} + {{targetElement.display}}
-
diff --git a/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.scss b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts similarity index 76% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts rename to src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts index ad45d1d8fc..b9474911bc 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts +++ b/src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts @@ -4,23 +4,27 @@ import { Router } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { distinctUntilChanged, take } from 'rxjs/operators'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { SuggestionTarget } from '../../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestion-targets/publication-claim/publication-claim.component.ts import { hasValue } from '../../../shared/empty.util'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SuggestionTargetsStateService } from './suggestion-targets.state.service'; +import { SuggestionTargetsStateService } from '../suggestion-targets.state.service'; import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths'; -import { SuggestionsService } from '../suggestions.service'; +import { SuggestionsService } from '../../suggestions.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; /** * Component to display the Suggestion Target list. */ @Component({ - selector: 'ds-suggestion-target', - templateUrl: './suggestion-targets.component.html', - styleUrls: ['./suggestion-targets.component.scss'], + selector: 'ds-publication-claim', + templateUrl: './publication-claim.component.html', + styleUrls: ['./publication-claim.component.scss'], }) -export class SuggestionTargetsComponent implements OnInit { +export class PublicationClaimComponent implements OnInit { /** * The source for which to list targets @@ -69,16 +73,11 @@ export class SuggestionTargetsComponent implements OnInit { * Component initialization. */ ngOnInit(): void { - this.targets$ = this.suggestionTargetsStateService.getReciterSuggestionTargets(); - this.totalElements$ = this.suggestionTargetsStateService.getReciterSuggestionTargetsTotals(); - } + this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(); + this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(); - /** - * First Suggestion Targets loading after view initialization. - */ - ngAfterViewInit(): void { this.subs.push( - this.suggestionTargetsStateService.isReciterSuggestionTargetsLoaded().pipe( + this.suggestionTargetsStateService.isSuggestionTargetsLoaded().pipe( take(1) ).subscribe(() => { this.getSuggestionTargets(); @@ -93,7 +92,7 @@ export class SuggestionTargetsComponent implements OnInit { * 'true' if the targets are loading, 'false' otherwise. */ public isTargetsLoading(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(); + return this.suggestionTargetsStateService.isSuggestionTargetsLoading(); } /** @@ -103,7 +102,7 @@ export class SuggestionTargetsComponent implements OnInit { * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ public isTargetsProcessing(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(); + return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(); } /** @@ -111,10 +110,8 @@ export class SuggestionTargetsComponent implements OnInit { * * @param {string} id * the id of suggestion target - * @param {string} name - * the name of suggestion target */ - public redirectToSuggestions(id: string, name: string) { + public redirectToSuggestions(id: string) { this.router.navigate([getSuggestionPageRoute(id)]); } @@ -136,8 +133,7 @@ export class SuggestionTargetsComponent implements OnInit { distinctUntilChanged(), take(1) ).subscribe((options: PaginationComponentOptions) => { - console.log('HELLO suggestion called!', options); - this.suggestionTargetsStateService.dispatchRetrieveReciterSuggestionTargets( + this.suggestionTargetsStateService.dispatchRetrieveSuggestionTargets( this.source, options.pageSize, options.currentPage diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts similarity index 93% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts rename to src/app/notifications/suggestion-targets/suggestion-targets.actions.ts index f65df98423..fe631f65da 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.actions.ts @@ -1,7 +1,12 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts import { type } from '../../../shared/ngrx/type'; import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { type } from '../../shared/ngrx/type'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.actions.ts /** * For each action type in an action group, make a simple diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts similarity index 81% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts rename to src/app/notifications/suggestion-targets/suggestion-targets.effects.ts index 05793afd6b..257501dee9 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.effects.ts @@ -9,15 +9,19 @@ import { of } from 'rxjs'; import { AddTargetAction, AddUserSuggestionsAction, - RefreshUserSuggestionsAction, RetrieveAllTargetsErrorAction, RetrieveTargetsBySourceAction, SuggestionTargetActionTypes, } from './suggestion-targets.actions'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { PaginatedList } from '../../core/data/paginated-list.model'; import { SuggestionsService } from '../suggestions.service'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.effects.ts /** * Provides effect methods for the Suggestion Targets actions. @@ -55,7 +59,7 @@ export class SuggestionTargetsEffects { retrieveAllTargetsErrorAction$ = createEffect(() => this.actions$.pipe( ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR), tap(() => { - this.notificationsService.error(null, this.translate.get('reciter.suggestion.target.error.service.retrieve')); + this.notificationsService.error(null, this.translate.get('suggestion.target.error.service.retrieve')); }) ), { dispatch: false }); @@ -64,7 +68,7 @@ export class SuggestionTargetsEffects { */ refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe( ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), - switchMap((action: RefreshUserSuggestionsAction) => { + switchMap(() => { return this.store$.select((state: any) => state.core.auth.userId) .pipe( switchMap((userId: string) => { @@ -76,7 +80,12 @@ export class SuggestionTargetsEffects { }), catchError((errors) => of(errors)) ); +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts })), { dispatch: false }); +======== + })) + ); +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.effects.ts /** * Initialize the effect class variables. diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts b/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts similarity index 90% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts rename to src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts index 4cf91f8ea3..05f7ac7351 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts @@ -1,5 +1,9 @@ import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.reducer.ts /** * The interface representing the OpenAIRE suggestion targets state. diff --git a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts similarity index 54% rename from src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts rename to src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts index 745e541df6..56668812a3 100644 --- a/src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts @@ -7,20 +7,28 @@ import { map } from 'rxjs/operators'; import { getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsVisitedSelector, - getReciterSuggestionTargetCurrentPageSelector, - getReciterSuggestionTargetTotalsSelector, - isReciterSuggestionTargetLoadedSelector, + getSuggestionTargetCurrentPageSelector, + getSuggestionTargetTotalsSelector, + isSuggestionTargetLoadedSelector, isReciterSuggestionTargetProcessingSelector, - reciterSuggestionTargetObjectSelector + suggestionTargetObjectSelector } from '../selectors'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts import { ClearSuggestionTargetsAction, MarkUserSuggestionsAsVisitedAction, RefreshUserSuggestionsAction, RetrieveTargetsBySourceAction } from './suggestion-targets.actions'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts import { SuggestionNotificationsState } from '../../notifications.reducer'; +======== +import { SuggestionNotificationsState } from '../../notifications/notifications.reducer'; +>>>>>>>> main:src/app/notifications/suggestion-targets/suggestion-targets.state.service.ts /** * The service handling the Suggestion targets State. @@ -35,80 +43,80 @@ export class SuggestionTargetsStateService { constructor(private store: Store) { } /** - * Returns the list of Reciter Suggestion Targets from the state. + * Returns the list of Suggestion Targets from the state. * - * @return Observable - * The list of Reciter Suggestion Targets. + * @return Observable + * The list of Suggestion Targets. */ - public getReciterSuggestionTargets(): Observable { - return this.store.pipe(select(reciterSuggestionTargetObjectSelector())); + public getSuggestionTargets(): Observable { + return this.store.pipe(select(suggestionTargetObjectSelector())); } /** - * Returns the information about the loading status of the Reciter Suggestion Targets (if it's running or not). + * 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 isReciterSuggestionTargetsLoading(): Observable { + public isSuggestionTargetsLoading(): Observable { return this.store.pipe( - select(isReciterSuggestionTargetLoadedSelector), + select(isSuggestionTargetLoadedSelector), map((loaded: boolean) => !loaded) ); } /** - * Returns the information about the loading status of the Reciter Suggestion Targets (whether or not they were 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 isReciterSuggestionTargetsLoaded(): Observable { - return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector)); + public isSuggestionTargetsLoaded(): Observable { + return this.store.pipe(select(isSuggestionTargetLoadedSelector)); } /** - * Returns the information about the processing status of the Reciter Suggestion Targets (if it's running or not). + * 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 isReciterSuggestionTargetsProcessing(): Observable { + public isSuggestionTargetsProcessing(): Observable { return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector)); } /** - * Returns, from the state, the total available pages of the Reciter Suggestion Targets. + * Returns, from the state, the total available pages of the Suggestion Targets. * * @return Observable - * The number of the Reciter Suggestion Targets pages. + * The number of the Suggestion Targets pages. */ - public getReciterSuggestionTargetsTotalPages(): Observable { - return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector)); + public getSuggestionTargetsTotalPages(): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector)); } /** - * Returns the current page of the Reciter Suggestion Targets, from the state. + * Returns the current page of the Suggestion Targets, from the state. * * @return Observable - * The number of the current Reciter Suggestion Targets page. + * The number of the current Suggestion Targets page. */ - public getReciterSuggestionTargetsCurrentPage(): Observable { - return this.store.pipe(select(getReciterSuggestionTargetCurrentPageSelector)); + public getSuggestionTargetsCurrentPage(): Observable { + return this.store.pipe(select(getSuggestionTargetCurrentPageSelector)); } /** - * Returns the total number of the Reciter Suggestion Targets. + * Returns the total number of the Suggestion Targets. * * @return Observable - * The number of the Reciter Suggestion Targets. + * The number of the Suggestion Targets. */ - public getReciterSuggestionTargetsTotals(): Observable { - return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector)); + public getSuggestionTargetsTotals(): Observable { + return this.store.pipe(select(getSuggestionTargetTotalsSelector)); } /** - * Dispatch a request to change the Reciter Suggestion Targets state, retrieving the targets from the server. + * 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 @@ -117,15 +125,15 @@ export class SuggestionTargetsStateService { * @param currentPage * The number of the current page. */ - public dispatchRetrieveReciterSuggestionTargets(source: string, elementsPerPage: number, currentPage: number): void { + public dispatchRetrieveSuggestionTargets(source: string, elementsPerPage: number, currentPage: number): void { this.store.dispatch(new RetrieveTargetsBySourceAction(source, elementsPerPage, currentPage)); } /** - * Returns, from the state, the reciter suggestion targets for the current user. + * Returns, from the state, the suggestion targets for the current user. * * @return Observable - * The Reciter Suggestion Targets object. + * The Suggestion Targets object. */ public getCurrentUserSuggestionTargets(): Observable { return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); diff --git a/src/app/notifications/suggestion.service.spec.ts b/src/app/notifications/suggestion.service.spec.ts new file mode 100644 index 0000000000..268eb59ddd --- /dev/null +++ b/src/app/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/notifications/suggestions-notification/suggestions-notification.component.html b/src/app/notifications/suggestions-notification/suggestions-notification.component.html new file mode 100644 index 0000000000..838bdb95ad --- /dev/null +++ b/src/app/notifications/suggestions-notification/suggestions-notification.component.html @@ -0,0 +1,9 @@ + + +
+
+ {{ 'notification.suggestion.please' | translate }} + {{ 'notification.suggestion.review' | translate}} +
+
+
diff --git a/src/app/notifications/suggestions-notification/suggestions-notification.component.scss b/src/app/notifications/suggestions-notification/suggestions-notification.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts b/src/app/notifications/suggestions-notification/suggestions-notification.component.ts similarity index 66% rename from src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts rename to src/app/notifications/suggestions-notification/suggestions-notification.component.ts index 74c62e4de3..3d0e233ad7 100644 --- a/src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts +++ b/src/app/notifications/suggestions-notification/suggestions-notification.component.ts @@ -1,11 +1,17 @@ import { Component, OnInit } from '@angular/core'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts import { SuggestionTarget } from '../../../core/notifications/reciter-suggestions/models/suggestion-target.model'; import { TranslateService } from '@ngx-translate/core'; +======== +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/notifications/suggestions-notification/suggestions-notification.component.ts import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.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', @@ -13,22 +19,19 @@ import { Observable } from 'rxjs'; }) export class SuggestionsNotificationComponent implements OnInit { - labelPrefix = 'mydspace.'; - /** * The user suggestion targets. */ suggestionsRD$: Observable; constructor( - private translateService: TranslateService, - private reciterSuggestionStateService: SuggestionTargetsStateService, - private notificationsService: NotificationsService, + private suggestionTargetsStateService: SuggestionTargetsStateService, private suggestionsService: SuggestionsService ) { } ngOnInit() { - this.suggestionsRD$ = this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(); + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.suggestionsRD$ = this.suggestionTargetsStateService.getCurrentUserSuggestionTargets(); } /** diff --git a/src/app/notifications/suggestions-popup/suggestions-popup.component.html b/src/app/notifications/suggestions-popup/suggestions-popup.component.html new file mode 100644 index 0000000000..474dfa329e --- /dev/null +++ b/src/app/notifications/suggestions-popup/suggestions-popup.component.html @@ -0,0 +1,27 @@ +
+ +
+ + diff --git a/src/app/notifications/suggestions-popup/suggestions-popup.component.scss b/src/app/notifications/suggestions-popup/suggestions-popup.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts b/src/app/notifications/suggestions-popup/suggestions-popup.component.spec.ts similarity index 87% rename from src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts rename to src/app/notifications/suggestions-popup/suggestions-popup.component.spec.ts index 242698b234..c8d59115d8 100644 --- a/src/app/notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts +++ b/src/app/notifications/suggestions-popup/suggestions-popup.component.spec.ts @@ -3,11 +3,9 @@ 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 { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { mockSuggestionTargetsObjectOne } from '../../../shared/mocks/reciter-suggestion-targets.mock'; +import { mockSuggestionTargetsObjectOne } from '../../shared/mocks/publication-claim-targets.mock'; import { SuggestionsService } from '../suggestions.service'; describe('SuggestionsPopupComponent', () => { @@ -34,7 +32,6 @@ describe('SuggestionsPopupComponent', () => { providers: [ { provide: SuggestionTargetsStateService, useValue: suggestionStateService }, { provide: SuggestionsService, useValue: suggestionService }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] @@ -61,10 +58,10 @@ describe('SuggestionsPopupComponent', () => { 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; @@ -72,7 +69,7 @@ describe('SuggestionsPopupComponent', () => { }); it('should show a notification when new publication suggestions are available', () => { - expect((component as any).notificationsService.success).toHaveBeenCalled(); + expect(suggestionStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction).toHaveBeenCalled(); }); diff --git a/src/app/notifications/suggestions-popup/suggestions-popup.component.ts b/src/app/notifications/suggestions-popup/suggestions-popup.component.ts new file mode 100644 index 0000000000..2cf3db128e --- /dev/null +++ b/src/app/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/notifications/reciter-suggestions/suggestions.service.ts b/src/app/notifications/suggestions.service.ts similarity index 80% rename from src/app/notifications/reciter-suggestions/suggestions.service.ts rename to src/app/notifications/suggestions.service.ts index 2b51b59b34..b1b03915f7 100644 --- a/src/app/notifications/reciter-suggestions/suggestions.service.ts +++ b/src/app/notifications/suggestions.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { of, forkJoin, Observable } from 'rxjs'; import { catchError, map, mergeMap, take } from 'rxjs/operators'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestions.service.ts 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'; @@ -10,11 +11,20 @@ import { SuggestionTarget } from '../../core/notifications/reciter-suggestions/m import { AuthService } from '../../core/auth/auth.service'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; +======== +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'; +>>>>>>>> main:src/app/notifications/suggestions.service.ts import { getAllSucceededRemoteDataPayload, getFinishedRemoteData, getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/suggestions.service.ts } from '../../core/shared/operators'; import { Suggestion } from '../../core/notifications/reciter-suggestions/models/suggestion.model'; import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; @@ -34,7 +44,29 @@ import { import { SuggestionsDataService } from '../../core/notifications/reciter-suggestions/suggestions-data.service'; +======== +} 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'; +>>>>>>>> main:src/app/notifications/suggestions.service.ts +/** + * useful for multiple approvals and ignores operation + * */ export interface SuggestionBulkResult { success: number; fails: number; @@ -48,17 +80,14 @@ export class SuggestionsService { /** * Initialize the service variables. - * @param {AuthService} authService * @param {ResearcherProfileDataService} researcherProfileService - * @param {SuggestionSourceDataService} suggestionSourceDataService * @param {SuggestionTargetDataService} suggestionTargetDataService * @param {SuggestionsDataService} suggestionsDataService + * @param translateService */ constructor( - private authService: AuthService, private researcherProfileService: ResearcherProfileDataService, private suggestionsDataService: SuggestionsDataService, - private suggestionSourceDataService: SuggestionSourceDataService, private suggestionTargetDataService: SuggestionTargetDataService, private translateService: TranslateService ) { @@ -157,7 +186,7 @@ export class SuggestionsService { * The EPerson id for which to retrieve suggestion targets */ public retrieveCurrentUserSuggestions(userUuid: string): Observable { - return this.researcherProfileService.findById(userUuid).pipe( + return this.researcherProfileService.findById(userUuid, true).pipe( getFirstSucceededRemoteDataPayload(), mergeMap((profile: ResearcherProfile) => { if (isNotEmpty(profile)) { @@ -191,7 +220,7 @@ export class SuggestionsService { return workspaceitemService.importExternalSourceEntry(suggestion.externalSourceUri, resolvedCollectionId) .pipe( getFirstSucceededRemoteDataPayload(), - catchError((error) => of(null)) + catchError(() => of(null)) ); } @@ -199,9 +228,9 @@ export class SuggestionsService { * Perform the delete operation over a single suggestion. * @param suggestionId */ - public notMine(suggestionId): Observable> { + public ignoreSuggestion(suggestionId): Observable> { return this.deleteReviewedSuggestion(suggestionId).pipe( - catchError((error) => of(null)) + catchError(() => of(null)) ); } @@ -226,11 +255,11 @@ export class SuggestionsService { } /** - * Perform a bulk notMine operation. + * Perform a bulk ignoreSuggestion operation. * @param suggestions the array containing the suggestions */ - public notMineMultiple(suggestions: Suggestion[]): Observable { - return forkJoin(suggestions.map((suggestion: Suggestion) => this.notMine(suggestion.id))) + 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, @@ -260,16 +289,17 @@ export class SuggestionsService { source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)), type: this.translateService.instant(this.translateSuggestionType(suggestionTarget.source)), suggestionId: suggestionTarget.id, - displayName: suggestionTarget.display + displayName: suggestionTarget.display, + url: getSuggestionPageRoute(suggestionTarget.id) }; } public translateSuggestionType(source: string): string { - return 'reciter.suggestion.type.' + source; + return 'suggestion.type.' + source; } public translateSuggestionSource(source: string): string { - return 'reciter.suggestion.source.' + source; + return 'suggestion.source.' + source; } /** diff --git a/src/app/profile-page/profile-page.module.ts b/src/app/profile-page/profile-page.module.ts index 4d3f02fbeb..126cf7bd8a 100644 --- a/src/app/profile-page/profile-page.module.ts +++ b/src/app/profile-page/profile-page.module.ts @@ -14,16 +14,15 @@ import { UiSwitchModule } from 'ngx-ui-switch'; import { ProfileClaimItemModalComponent } from './profile-claim-item-modal/profile-claim-item-modal.component'; import { NotificationsModule } from '../notifications/notifications.module'; - @NgModule({ - imports: [ - ProfilePageRoutingModule, - CommonModule, - SharedModule, - FormModule, - UiSwitchModule, - NotificationsModule - ], + imports: [ + ProfilePageRoutingModule, + CommonModule, + SharedModule, + FormModule, + UiSwitchModule, + NotificationsModule + ], exports: [ ProfilePageComponent, ThemedProfilePageComponent, diff --git a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts index f524cd56c2..d7cbf846e4 100644 --- a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts +++ b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts @@ -4,7 +4,11 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/r /** * Interface for the route parameters. */ +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts export interface NotificationsSuggestionTargetsPageParams { +======== +export interface AdminNotificationsPublicationClaimPageParams { +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts pageId?: string; pageSize?: number; currentPage?: number; @@ -14,7 +18,11 @@ export interface NotificationsSuggestionTargetsPageParams { * This class represents a resolver that retrieve the route data before the route is activated. */ @Injectable() +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts export class NotificationsSuggestionTargetsPageResolver implements Resolve { +======== +export class AdminNotificationsPublicationClaimPageResolver implements Resolve { +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts /** * Method for resolving the parameters in the current route. @@ -22,7 +30,11 @@ export class NotificationsSuggestionTargetsPageResolver implements Resolve>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts return { pageId: route.queryParams.pageId, pageSize: parseInt(route.queryParams.pageSize, 10), diff --git a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts index f1b01c747d..6061bda063 100644 --- a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts +++ b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts @@ -1,13 +1,23 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts import { NotificationsSuggestionTargetsPageComponent } from './notifications-suggestion-targets-page.component'; +======== +import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component'; +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts describe('NotificationsSuggestionTargetsPageComponent', () => { let component: NotificationsSuggestionTargetsPageComponent; let fixture: ComponentFixture; +======== +describe('AdminNotificationsPublicationClaimPageComponent', () => { + let component: AdminNotificationsPublicationClaimPageComponent; + let fixture: ComponentFixture; +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts beforeEach(async(() => { TestBed.configureTestingModule({ @@ -16,10 +26,17 @@ describe('NotificationsSuggestionTargetsPageComponent', () => { TranslateModule.forRoot() ], declarations: [ +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts NotificationsSuggestionTargetsPageComponent ], providers: [ NotificationsSuggestionTargetsPageComponent +======== + AdminNotificationsPublicationClaimPageComponent + ], + providers: [ + AdminNotificationsPublicationClaimPageComponent +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts ], schemas: [NO_ERRORS_SCHEMA] }) @@ -27,7 +44,11 @@ describe('NotificationsSuggestionTargetsPageComponent', () => { })); beforeEach(() => { +<<<<<<<< HEAD:src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page.component.spec.ts fixture = TestBed.createComponent(NotificationsSuggestionTargetsPageComponent); +======== + fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent); +>>>>>>>> main:src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component.spec.ts component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index e2dbaaa0ff..edefff8592 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -5,13 +5,14 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model' import { Item } from '../../../core/shared/item.model'; import { DSOSelectorModalWrapperComponent, SelectorActionType } from './dso-selector-modal-wrapper.component'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { By } from '@angular/platform-browser'; import { DSOSelectorComponent } from '../dso-selector/dso-selector.component'; import { MockComponent } from 'ng-mocks'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; +import { hasValue } from '../../empty.util'; describe('DSOSelectorModalWrapperComponent', () => { let component: DSOSelectorModalWrapperComponent; @@ -83,6 +84,20 @@ describe('DSOSelectorModalWrapperComponent', () => { }); }); + describe('selectObject with emit only', () => { + beforeEach(() => { + spyOn(component, 'navigate'); + spyOn(component, 'close'); + spyOn(component.select, 'emit'); + component.emitOnly = true; + component.selectObject(item); + }); + it('should call the close and navigate method on the component with the given DSO', () => { + expect(component.close).toHaveBeenCalled(); + expect(component.select.emit).toHaveBeenCalledWith(item); + }); + }); + describe('close', () => { beforeEach(() => { component.close(); @@ -113,6 +128,19 @@ describe('DSOSelectorModalWrapperComponent', () => { expect(component.close).toHaveBeenCalled(); }); }); + + describe('should find route data', () => { + beforeEach(() => { + spyOn(component, 'findRouteData'); + component.ngOnInit(); + }); + it('should call the findRouteData method on the component', () => { + expect(component.findRouteData).toHaveBeenCalled(); + }); + it('should return undefined', () => { + expect(component.findRouteData((route) => hasValue(route.data), {} as unknown as ActivatedRouteSnapshot)).toEqual(undefined); + }); + }); }); @Component({ diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.ts index 7cad7a9783..1a08740a94 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.ts @@ -96,7 +96,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { private pageConfigSub: Subscription; /** - * Initialize instance variables and inject the properly DataService + * Initialize instance variables and inject the properly UpdateDataServiceImpl * * @param {DSONameService} dsoNameService * @param {Injector} parentInjector diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts index 6a0324d2ac..6a67d84c28 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.spec.ts @@ -54,4 +54,31 @@ describe('FilterInputSuggestionsComponent', () => { expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex]); }); }); + + describe('component methods', () => { + const testData = { + value: 'test-field' + } as unknown as any; + + beforeEach(() => { + spyOn(comp.submitSuggestion, 'emit'); + spyOn(comp.clickSuggestion, 'emit'); + spyOn(comp, 'close'); + }); + + it('should properly submit', () => { + comp.onSubmit(testData); + expect(comp.submitSuggestion.emit).toHaveBeenCalledWith(testData); + expect(comp.value).toBe(testData); + }); + + it('should update value on suggestion clicked', () => { + comp.onClickSuggestion(testData); + expect(comp.clickSuggestion.emit).toHaveBeenCalledWith(testData); + expect(comp.value).toBe(testData.value); + expect(comp.blockReopen).toBeTruthy(); + expect(comp.close).toHaveBeenCalled(); + }); + }); + }); diff --git a/src/app/shared/mocks/reciter-suggestion-targets.mock.ts b/src/app/shared/mocks/publication-claim-targets.mock.ts similarity index 93% rename from src/app/shared/mocks/reciter-suggestion-targets.mock.ts rename to src/app/shared/mocks/publication-claim-targets.mock.ts index f6810f9912..1d7688c1e3 100644 --- a/src/app/shared/mocks/reciter-suggestion-targets.mock.ts +++ b/src/app/shared/mocks/publication-claim-targets.mock.ts @@ -1,5 +1,5 @@ import { ResourceType } from '../../core/shared/resource-type'; -import { SuggestionTarget } from '../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model'; // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- diff --git a/src/app/shared/mocks/reciter-suggestion.mock.ts b/src/app/shared/mocks/publication-claim.mock.ts similarity index 96% rename from src/app/shared/mocks/reciter-suggestion.mock.ts rename to src/app/shared/mocks/publication-claim.mock.ts index 652871e643..143b54d8dd 100644 --- a/src/app/shared/mocks/reciter-suggestion.mock.ts +++ b/src/app/shared/mocks/publication-claim.mock.ts @@ -2,11 +2,11 @@ // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- -import { Suggestion } from '../../core/notifications/reciter-suggestions/models/suggestion.model'; -import { SUGGESTION } from '../../core/notifications/reciter-suggestions/models/suggestion-objects.resource-type'; +import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model'; +import { SUGGESTION } from '../../core/suggestion-notifications/models/suggestion-objects.resource-type'; export const mockSuggestionPublicationOne: Suggestion = { - id: '24694772', + id: '24694773', display: 'publication one', source: 'reciter', externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772', diff --git a/src/app/shared/mocks/suggestion.mock.ts b/src/app/shared/mocks/suggestion.mock.ts index 8b1ab7acd5..ed7f9045d5 100644 --- a/src/app/shared/mocks/suggestion.mock.ts +++ b/src/app/shared/mocks/suggestion.mock.ts @@ -1,6 +1,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Item } from '../../core/shared/item.model'; import { SearchResult } from '../search/models/search-result.model'; +import { of as observableOf } from 'rxjs'; // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- @@ -1333,7 +1334,8 @@ export function getMockSuggestionNotificationsStateService(): any { getOpenaireBrokerTopicsCurrentPage: jasmine.createSpy('getOpenaireBrokerTopicsCurrentPage'), getOpenaireBrokerTopicsTotals: jasmine.createSpy('getOpenaireBrokerTopicsTotals'), dispatchRetrieveOpenaireBrokerTopics: jasmine.createSpy('dispatchRetrieveOpenaireBrokerTopics'), - dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction') + dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction'), + dispatchRefreshUserSuggestionsAction: undefined }); } /** @@ -1342,10 +1344,17 @@ export function getMockSuggestionNotificationsStateService(): any { export function getMockSuggestionsService(): any { return jasmine.createSpyObj('SuggestionsService', { getTargets: jasmine.createSpy('getTargets'), - getSuggestions: jasmine.createSpy('getSuggestions'), + getSuggestions: observableOf([]), clearSuggestionRequests: jasmine.createSpy('clearSuggestionRequests'), deleteReviewedSuggestion: jasmine.createSpy('deleteReviewedSuggestion'), retrieveCurrentUserSuggestions: jasmine.createSpy('retrieveCurrentUserSuggestions'), getTargetUuid: jasmine.createSpy('getTargetUuid'), + ignoreSuggestion: observableOf(null), + ignoreSuggestionMultiple: observableOf({success: 1, fails: 0}), + approveAndImportMultiple: observableOf({success: 1, fails: 0}), + approveAndImport: observableOf({id: '1234'}), + isCollectionFixed: false, + translateSuggestionSource: 'testSource', + translateSuggestionType: 'testType', }); } diff --git a/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts b/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts index 6408dddac6..987ca1411e 100644 --- a/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts +++ b/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts @@ -13,7 +13,7 @@ import { CacheableObject } from '../../core/cache/cacheable-object.model'; import { IdentifiableDataService } from '../../core/data/base/identifiable-data.service'; /** - * Class to return DataService for given ResourceType + * Class to return UpdateDataServiceImpl for given ResourceType */ export class MyDSpaceActionsServiceFactory> { public getConstructor(type: ResourceType): TService { diff --git a/src/app/shared/mydspace-actions/mydspace-actions.ts b/src/app/shared/mydspace-actions/mydspace-actions.ts index 931929ccce..184f059bd8 100644 --- a/src/app/shared/mydspace-actions/mydspace-actions.ts +++ b/src/app/shared/mydspace-actions/mydspace-actions.ts @@ -47,7 +47,7 @@ export abstract class MyDSpaceActionsComponent(false); /** - * Instance of DataService related to mydspace object + * Instance of UpdateDataServiceImpl related to mydspace object */ protected objectDataService: TService; diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component.ts index 0aa131d428..f635c027b5 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component.ts @@ -4,6 +4,7 @@ import { FilterType } from '../../../models/filter-type.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, REFRESH_FILTER } from '../../../../../core/shared/search/search-filter.service'; @@ -35,6 +36,11 @@ export class SearchFacetFilterWrapperComponent implements OnInit { */ @Input() refreshFilters: BehaviorSubject; + /** + * The current scope + */ + @Input() scope: string; + /** * The constructor of the search facet filter that should be rendered, based on the filter config's type */ @@ -56,7 +62,8 @@ export class SearchFacetFilterWrapperComponent implements OnInit { providers: [ { provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }, { provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] }, - { provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] } + { provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] }, + { provide: SCOPE, useFactory: () => (this.scope), deps: [] }, ], parent: this.injector }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index 92d2e5265b..fc348722e5 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, REFRESH_FILTER, SearchFilterService @@ -99,6 +100,7 @@ describe('SearchFacetFilterComponent', () => { { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) }, + { provide: SCOPE, useValue: undefined }, { provide: SearchFilterService, useValue: { getSelectedValuesForFilter: () => observableOf(selectedValues), diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 2b2eb9b11a..994b488d9c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -22,6 +22,7 @@ import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchService } from '../../../../../core/shared/search/search.service'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, REFRESH_FILTER, SearchFilterService @@ -104,7 +105,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, - @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject) { + @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject, + @Inject(SCOPE) public scope: string, + ) { } /** @@ -114,8 +117,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.currentUrl = this.router.url; this.filterValues$ = new BehaviorSubject(createPendingRemoteDataObject()); this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged()); - - this.searchOptions$ = this.searchConfigService.searchOptions; + this.searchOptions$ = this.searchConfigService.searchOptions.pipe( + map((options: SearchOptions) => hasNoValue(this.scope) ? options : Object.assign({}, options, { + scope: this.scope, + })), + ); this.subs.push( this.searchOptions$.subscribe(() => this.updateFilterValueList()), this.refreshFilters.asObservable().pipe( diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index a352c5587b..97809ef854 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -19,6 +19,7 @@ (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }"> diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts index d1d3bd729d..67e8906bb5 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts @@ -6,7 +6,7 @@ import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { SearchFilterConfig } from '../../models/search-filter-config.model'; import { SearchFilterService } from '../../../../core/shared/search/search-filter.service'; import { slide } from '../../../animations/slide'; -import { isNotEmpty } from '../../../empty.util'; +import { isNotEmpty, hasValue } from '../../../empty.util'; import { SearchService } from '../../../../core/shared/search/search.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; @@ -38,6 +38,11 @@ export class SearchFilterComponent implements OnInit { */ @Input() refreshFilters: BehaviorSubject; + /** + * The current scope + */ + @Input() scope: string; + /** * True when the filter is 100% collapsed in the UI */ @@ -171,6 +176,9 @@ export class SearchFilterComponent implements OnInit { } else { return this.searchConfigService.searchOptions.pipe( switchMap((options) => { + if (hasValue(this.scope)) { + options.scope = this.scope; + } return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe( filter((RD) => !RD.isLoading), map((valuesRD) => { diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index e6c74d8047..4469a124ce 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common'; import { SearchService } from '../../../../../core/shared/search/search.service'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, SearchFilterService, REFRESH_FILTER @@ -75,7 +76,8 @@ describe('SearchHierarchyFilterComponent', () => { { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, - { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false)} + { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false)}, + { provide: SCOPE, useValue: undefined }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index f9b3f2bff9..d53fa37cf4 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -9,6 +9,7 @@ import { import { SearchService } from '../../../../../core/shared/search/search.service'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, SearchFilterService, REFRESH_FILTER } from '../../../../../core/shared/search/search-filter.service'; @@ -49,9 +50,10 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, - @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject + @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject, + @Inject(SCOPE) public scope: string, ) { - super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, scope); } vocabularyExists$: Observable; diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 3a146f5059..03f47a7569 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, REFRESH_FILTER, SearchFilterService @@ -105,6 +106,7 @@ describe('SearchRangeFilterComponent', () => { { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) }, + { provide: SCOPE, useValue: undefined }, { provide: SearchFilterService, useValue: { getSelectedValuesForFilter: () => selectedValues, diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index ed20e63c52..a6eaf436c9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -10,6 +10,7 @@ import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/se import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { FILTER_CONFIG, + SCOPE, IN_PLACE_SEARCH, REFRESH_FILTER, SearchFilterService @@ -101,8 +102,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @Inject(PLATFORM_ID) private platformId: any, @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject, + @Inject(SCOPE) public scope: string, private route: RouteService) { - super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, scope); } diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index e392cd2663..c006d80c44 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -1,7 +1,7 @@

{{"search.filters.head" | translate}}

- +
{{"search.filters.reset" | translate}} diff --git a/src/app/shared/search/search-settings/search-settings.component.spec.ts b/src/app/shared/search/search-settings/search-settings.component.spec.ts index d0b51f04b1..bdcdb6f171 100644 --- a/src/app/shared/search/search-settings/search-settings.component.spec.ts +++ b/src/app/shared/search/search-settings/search-settings.component.spec.ts @@ -1,4 +1,3 @@ -import { SearchService } from '../../../core/shared/search/search.service'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { SearchSettingsComponent } from './search-settings.component'; import { of as observableOf } from 'rxjs'; @@ -6,15 +5,11 @@ import { PaginationComponentOptions } from '../../pagination/pagination-componen import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; -import { ActivatedRoute } from '@angular/router'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { By } from '@angular/platform-browser'; -import { SearchFilterService } from '../../../core/shared/search/search-filter.service'; import { VarDirective } from '../../utils/var.directive'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; -import { SidebarService } from '../../sidebar/sidebar.service'; -import { SidebarServiceStub } from '../../testing/sidebar-service.stub'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../testing/pagination-service.stub'; @@ -22,32 +17,23 @@ describe('SearchSettingsComponent', () => { let comp: SearchSettingsComponent; let fixture: ComponentFixture; - let searchServiceObject: SearchService; let pagination: PaginationComponentOptions; let sort: SortOptions; - let mockResults; - let searchServiceStub; let queryParam; let scopeParam; let paginatedSearchOptions; - let paginationService; + let paginationService: PaginationServiceStub; - let activatedRouteStub; - beforeEach(waitForAsync(() => { + beforeEach(waitForAsync(async () => { pagination = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; sort = new SortOptions('score', SortDirection.DESC); - mockResults = ['test', 'data']; - searchServiceStub = { - searchOptions: { pagination: pagination, sort: sort }, - search: () => mockResults, - }; queryParam = 'test query'; scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; @@ -58,30 +44,12 @@ describe('SearchSettingsComponent', () => { sort, }; - activatedRouteStub = { - queryParams: observableOf({ - query: queryParam, - scope: scopeParam, - }), - }; - paginationService = new PaginationServiceStub(pagination, sort); - TestBed.configureTestingModule({ + await TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective], providers: [ - { provide: SearchService, useValue: searchServiceStub }, - - { provide: ActivatedRoute, useValue: activatedRouteStub }, - { - provide: SidebarService, - useValue: SidebarServiceStub, - }, - { - provide: SearchFilterService, - useValue: {}, - }, { provide: PaginationService, useValue: paginationService, @@ -111,10 +79,7 @@ describe('SearchSettingsComponent', () => { // SearchPageComponent test instance fixture.detectChanges(); - searchServiceObject = (comp as any).service; spyOn(comp, 'reloadOrder'); - spyOn(searchServiceObject, 'search').and.callThrough(); - }); it('it should show the order settings with the respective selectable options', () => { diff --git a/src/app/shared/search/search-settings/search-settings.component.ts b/src/app/shared/search/search-settings/search-settings.component.ts index 0efd38b5b2..23c9e78c3a 100644 --- a/src/app/shared/search/search-settings/search-settings.component.ts +++ b/src/app/shared/search/search-settings/search-settings.component.ts @@ -1,7 +1,5 @@ import { Component, Inject, Input } from '@angular/core'; -import { SearchService } from '../../../core/shared/search/search.service'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; -import { ActivatedRoute, Router } from '@angular/router'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { PaginationService } from '../../../core/pagination/pagination.service'; @@ -26,11 +24,10 @@ export class SearchSettingsComponent { */ @Input() sortOptionsList: SortOptions[]; - constructor(private service: SearchService, - private route: ActivatedRoute, - private router: Router, - private paginationService: PaginationService, - @Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) { + constructor( + protected paginationService: PaginationService, + @Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService, + ) { } /** diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index d0d9bdda86..9d069636a9 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -36,8 +36,6 @@ import { getCollectionPageRoute } from '../../collection-page/collection-page-ro let comp: SearchComponent; let fixture: ComponentFixture; -let searchServiceObject: SearchService; -let searchConfigurationServiceObject: SearchConfigurationService; const store: Store = jasmine.createSpyObj('store', { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, @@ -93,7 +91,6 @@ const mockDso2 = Object.assign(new Item(), { } } }); -const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const mockSearchResults: SearchObjects = Object.assign(new SearchObjects(), { page: [mockDso, mockDso2] }); @@ -106,23 +103,13 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', { getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig), trackSearch: {}, }) as SearchService; -const configurationParam = 'default'; const queryParam = 'test query'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; -const fixedFilter = 'fixed filter'; const defaultSearchOptions = new PaginatedSearchOptions({ pagination }); const paginatedSearchOptions$ = new BehaviorSubject(defaultSearchOptions); -const paginatedSearchOptions = new PaginatedSearchOptions({ - configuration: configurationParam, - query: queryParam, - scope: scopeParam, - fixedFilter: fixedFilter, - pagination, - sort -}); const activatedRouteStub = { snapshot: { queryParamMap: new Map([ @@ -155,14 +142,11 @@ const filtersConfigRD = createSuccessfulRemoteDataObject([mockFilterConfig, mock const filtersConfigRD$ = observableOf(filtersConfigRD); const routeServiceStub = { - getRouteParameterValue: () => { - return observableOf(''); - }, getQueryParameterValue: () => { - return observableOf(''); + return observableOf(null); }, getQueryParamsWithPrefix: () => { - return observableOf(''); + return observableOf(null); }, setParameter: () => { return; @@ -252,16 +236,10 @@ describe('SearchComponent', () => { comp.paginationId = paginationId; spyOn((comp as any), 'getSearchOptions').and.returnValue(paginatedSearchOptions$.asObservable()); - - searchServiceObject = TestBed.inject(SearchService); - searchConfigurationServiceObject = TestBed.inject(SEARCH_CONFIG_SERVICE); - }); afterEach(() => { comp = null; - searchServiceObject = null; - searchConfigurationServiceObject = null; }); it('should init search parameters properly and call retrieveSearchResults', fakeAsync(() => { diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 61f3a119c8..fc07893d72 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output, OnDestroy } from '@angular/core'; import { NavigationStart, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; @@ -11,7 +11,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { pushInOut } from '../animations/push'; import { HostWindowService } from '../host-window.service'; import { SidebarService } from '../sidebar/sidebar.service'; -import { hasValue, hasValueOperator, isNotEmpty } from '../empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../empty.util'; import { RouteService } from '../../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { PaginatedSearchOptions } from './models/paginated-search-options.model'; @@ -34,7 +34,7 @@ import { CollectionElementLinkType } from '../object-collection/collection-eleme import { environment } from 'src/environments/environment'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; -import { WorkspaceItem } from '../..//core/submission/models/workspaceitem.model'; +import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; @@ -50,7 +50,7 @@ import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routi /** * This component renders a sidebar, a search input bar and the search results. */ -export class SearchComponent implements OnInit { +export class SearchComponent implements OnDestroy, OnInit { /** * The list of available configuration options @@ -166,6 +166,11 @@ export class SearchComponent implements OnInit { */ @Input() query: string; + /** + * The fallback scope when no scope is defined in the url, if this is also undefined no scope will be set + */ + @Input() scope: string; + /** * The current configuration used during the search */ @@ -179,7 +184,7 @@ export class SearchComponent implements OnInit { /** * The current sort options used */ - currentScope$: BehaviorSubject = new BehaviorSubject(''); + currentScope$: Observable; /** * The current sort options used @@ -299,6 +304,10 @@ export class SearchComponent implements OnInit { this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery); } + this.currentScope$ = this.routeService.getQueryParameterValue('scope').pipe( + map((routeValue: string) => hasValue(routeValue) ? routeValue : this.scope), + ); + this.isSidebarCollapsed$ = this.isSidebarCollapsed(); this.searchLink = this.getSearchLink(); this.currentContext$.next(this.context); @@ -306,9 +315,8 @@ export class SearchComponent implements OnInit { // Determinate PaginatedSearchOptions and listen to any update on it const configuration$: Observable = this.searchConfigService .getCurrentConfiguration(this.configuration).pipe(distinctUntilChanged()); - const searchSortOptions$: Observable = configuration$.pipe( - switchMap((configuration: string) => this.searchConfigService - .getConfigurationSearchConfig(configuration)), + const searchSortOptions$: Observable = combineLatest([configuration$, this.currentScope$]).pipe( + switchMap(([configuration, scope]: [string, string]) => this.searchConfigService.getConfigurationSearchConfig(configuration, scope)), map((searchConfig: SearchConfig) => this.searchConfigService.getConfigurationSortOptions(searchConfig)), distinctUntilChanged() ); @@ -321,13 +329,13 @@ export class SearchComponent implements OnInit { ); const searchOptions$: Observable = this.getSearchOptions().pipe(distinctUntilChanged()); - this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe( - filter(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => { + this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$, this.currentScope$]).pipe( + filter(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => { // filter for search options related to instanced paginated id return searchOptions.pagination.id === this.paginationId; }), debounceTime(100) - ).subscribe(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => { + ).subscribe(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => { // Build the PaginatedSearchOptions object const combinedOptions = Object.assign({}, searchOptions, { @@ -337,6 +345,9 @@ export class SearchComponent implements OnInit { if (combinedOptions.query === '') { combinedOptions.query = this.query; } + if (isEmpty(combinedOptions.scope)) { + combinedOptions.scope = scope; + } const newSearchOptions = new PaginatedSearchOptions(combinedOptions); // check if search options are changed // if so retrieve new related results otherwise skip it @@ -344,13 +355,12 @@ export class SearchComponent implements OnInit { // Initialize variables this.currentConfiguration$.next(configuration); this.currentSortOptions$.next(newSearchOptions.sort); - this.currentScope$.next(newSearchOptions.scope); this.sortOptionsList$.next(searchSortOptions); this.searchOptions$.next(newSearchOptions); this.initialized$.next(true); // retrieve results this.retrieveSearchResults(newSearchOptions); - this.retrieveFilters(searchOptions); + this.retrieveFilters(newSearchOptions); } })); diff --git a/src/app/shared/search/themed-search.component.ts b/src/app/shared/search/themed-search.component.ts index 03f6f37e25..ef0afad7b3 100644 --- a/src/app/shared/search/themed-search.component.ts +++ b/src/app/shared/search/themed-search.component.ts @@ -15,12 +15,38 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode */ @Component({ selector: 'ds-themed-search', - styleUrls: [], templateUrl: '../theme-support/themed.component.html', }) export class ThemedSearchComponent extends ThemedComponent { - protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query']; + protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = [ + 'configurationList', + 'context', + 'configuration', + 'fixedFilterQuery', + 'useCachedVersionIfAvailable', + 'inPlaceSearch', + 'linkType', + 'paginationId', + 'searchEnabled', + 'sideBarWidth', + 'searchFormPlaceholder', + 'selectable', + 'selectionConfig', + 'showCsvExport', + 'showSidebar', + 'showThumbnails', + 'showViewModes', + 'useUniquePageId', + 'viewModeList', + 'showScopeSelector', + 'trackStatistics', + 'query', + 'scope', + 'resultFound', + 'deselectObject', + 'selectObject', + ]; @Input() configurationList: SearchConfigurationOption[]; @@ -52,7 +78,7 @@ export class ThemedSearchComponent extends ThemedComponent { @Input() showSidebar: boolean; - @Input() showThumbnails; + @Input() showThumbnails: boolean; @Input() showViewModes: boolean; @@ -66,6 +92,8 @@ export class ThemedSearchComponent extends ThemedComponent { @Input() query: string; + @Input() scope: string; + @Output() resultFound: EventEmitter> = new EventEmitter(); @Output() deselectObject: EventEmitter = new EventEmitter(); diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index d008bf61f1..72c555f39c 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -80,6 +80,9 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { const fileData: any = mockUploadFiles[0]; const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex); + let noAccessConditionsMock = Object.assign({}, mockFileFormData); + delete noAccessConditionsMock.accessConditions; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -299,6 +302,28 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { })); + it('should update Bitstream data properly when access options are omitted', fakeAsync(() => { + compAsAny.formRef = {formGroup: null}; + compAsAny.fileData = fileData; + compAsAny.pathCombiner = pathCombiner; + formService.validateAllFormFields.and.callFake(() => null); + formService.isValid.and.returnValue(of(true)); + formService.getFormData.and.returnValue(of(noAccessConditionsMock)); + const response = [ + Object.assign(mockSubmissionObject, { + sections: { + upload: { + files: mockUploadFiles + } + } + }) + ]; + operationsService.jsonPatchByResourceID.and.returnValue(of(response)); + comp.saveBitstreamData(); + tick(); + expect(uploadService.updateFileData).toHaveBeenCalled(); + })); + it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => { compAsAny.formRef = {formGroup: null}; compAsAny.pathCombiner = pathCombiner; diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 9ee4a7dda5..eb4dc5aaf8 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -416,56 +416,58 @@ export class SubmissionSectionUploadFileEditComponent this.operationsBuilder.remove(this.pathCombiner.getPath(path)); }); const accessConditionsToSave = []; - formData.accessConditions - .map((accessConditions) => accessConditions.accessConditionGroup) - .filter((accessCondition) => isNotEmpty(accessCondition)) - .forEach((accessCondition) => { - let accessConditionOpt; + if (formData.hasOwnProperty('accessConditions')) { + formData.accessConditions + .filter((accessConditions) => isNotNull(accessConditions)) + .map((accessConditions) => accessConditions.accessConditionGroup) + .filter((accessCondition) => isNotEmpty(accessCondition)) + .forEach((accessCondition) => { + let accessConditionOpt; - this.availableAccessConditionOptions - .filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value) - .forEach((element) => accessConditionOpt = element); + this.availableAccessConditionOptions + .filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value) + .forEach((element) => accessConditionOpt = element); - if (accessConditionOpt) { - const currentAccessCondition = Object.assign({}, accessCondition); - currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name); + if (accessConditionOpt) { + const currentAccessCondition = Object.assign({}, accessCondition); + currentAccessCondition.name = this.retrieveValueFromField(accessCondition.name); - /* When start and end date fields are deactivated, their values may be still present in formData, - therefore it is necessary to delete them if they're not allowed by the current access condition option. */ - if (!accessConditionOpt.hasStartDate) { - delete currentAccessCondition.startDate; - } else if (accessCondition.startDate) { - const startDate = this.retrieveValueFromField(accessCondition.startDate); - // Clamp the start date to the maximum, if any, since the - // datepicker sometimes exceeds it. - let startDateDate = new Date(startDate); - if (accessConditionOpt.maxStartDate) { + /* When start and end date fields are deactivated, their values may be still present in formData, + therefore it is necessary to delete them if they're not allowed by the current access condition option. */ + if (!accessConditionOpt.hasStartDate) { + delete currentAccessCondition.startDate; + } else if (accessCondition.startDate) { + const startDate = this.retrieveValueFromField(accessCondition.startDate); + // Clamp the start date to the maximum, if any, since the + // datepicker sometimes exceeds it. + let startDateDate = new Date(startDate); + if (accessConditionOpt.maxStartDate) { const maxStartDateDate = new Date(accessConditionOpt.maxStartDate); if (startDateDate > maxStartDateDate) { - startDateDate = maxStartDateDate; + startDateDate = maxStartDateDate; } + } + currentAccessCondition.startDate = dateToISOFormat(startDateDate); } - currentAccessCondition.startDate = dateToISOFormat(startDateDate); - } - if (!accessConditionOpt.hasEndDate) { - delete currentAccessCondition.endDate; - } else if (accessCondition.endDate) { - const endDate = this.retrieveValueFromField(accessCondition.endDate); - // Clamp the end date to the maximum, if any, since the - // datepicker sometimes exceeds it. - let endDateDate = new Date(endDate); - if (accessConditionOpt.maxEndDate) { + if (!accessConditionOpt.hasEndDate) { + delete currentAccessCondition.endDate; + } else if (accessCondition.endDate) { + const endDate = this.retrieveValueFromField(accessCondition.endDate); + // Clamp the end date to the maximum, if any, since the + // datepicker sometimes exceeds it. + let endDateDate = new Date(endDate); + if (accessConditionOpt.maxEndDate) { const maxEndDateDate = new Date(accessConditionOpt.maxEndDate); if (endDateDate > maxEndDateDate) { - endDateDate = maxEndDateDate; + endDateDate = maxEndDateDate; } + } + currentAccessCondition.endDate = dateToISOFormat(endDateDate); } - currentAccessCondition.endDate = dateToISOFormat(endDateDate); + accessConditionsToSave.push(currentAccessCondition); } - accessConditionsToSave.push(currentAccessCondition); - } - }); - + }); + } if (isNotEmpty(accessConditionsToSave)) { this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); } diff --git a/src/app/notifications/reciter-suggestions/selectors.ts b/src/app/suggestion-notifications/selectors.ts similarity index 53% rename from src/app/notifications/reciter-suggestions/selectors.ts rename to src/app/suggestion-notifications/selectors.ts index f08914cb83..45d56a6793 100644 --- a/src/app/notifications/reciter-suggestions/selectors.ts +++ b/src/app/suggestion-notifications/selectors.ts @@ -1,44 +1,49 @@ import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/store'; +<<<<<<<< HEAD:src/app/notifications/reciter-suggestions/selectors.ts import { suggestionNotificationsSelector, SuggestionNotificationsState } from '../notifications.reducer'; import { SuggestionTarget } from '../../core/notifications/reciter-suggestions/models/suggestion-target.model'; +======== +import { suggestionNotificationsSelector, SuggestionNotificationsState } from '../notifications/notifications.reducer'; +import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model'; +>>>>>>>> main:src/app/suggestion-notifications/selectors.ts import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; -import {subStateSelector} from '../../submission/selectors'; +import {subStateSelector} from '../submission/selectors'; /** * Returns the Reciter Suggestion Target state. - * @function _getReciterSuggestionTargetState + * @function _getSuggestionTargetState * @param {AppState} state Top level state. * @return {SuggestionNotificationsState} */ -const _getReciterSuggestionTargetState = createFeatureSelector('suggestionNotifications'); +const _getSuggestionTargetState = createFeatureSelector('suggestionNotifications'); // Reciter Suggestion Targets // ---------------------------------------------------------------------------- /** - * Returns the Reciter Suggestion Targets State. - * @function reciterSuggestionTargetStateSelector + * Returns the Suggestion Targets State. + * @function suggestionTargetStateSelector * @return {SuggestionNotificationsState} */ -export function reciterSuggestionTargetStateSelector(): MemoizedSelector { +export function suggestionTargetStateSelector(): MemoizedSelector { return subStateSelector(suggestionNotificationsSelector, 'suggestionTarget'); } /** - * Returns the Reciter Suggestion Targets list. - * @function reciterSuggestionTargetObjectSelector + * Returns the Suggestion Targets list. + * @function suggestionTargetObjectSelector * @return {SuggestionTarget[]} */ -export function reciterSuggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); +export function suggestionTargetObjectSelector(): MemoizedSelector { + return subStateSelector(suggestionTargetStateSelector(), 'targets'); } /** - * Returns true if the Reciter Suggestion Targets are loaded. - * @function isReciterSuggestionTargetLoadedSelector + * Returns true if the Suggestion Targets are loaded. + * @function isSuggestionTargetLoadedSelector * @return {boolean} */ -export const isReciterSuggestionTargetLoadedSelector = createSelector(_getReciterSuggestionTargetState, +export const isSuggestionTargetLoadedSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.loaded ); @@ -47,51 +52,51 @@ export const isReciterSuggestionTargetLoadedSelector = createSelector(_getRecite * @function isDeduplicationSetsProcessingSelector * @return {boolean} */ -export const isReciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, +export const isReciterSuggestionTargetProcessingSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.processing ); /** * Returns the total available pages of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetTotalPagesSelector + * @function getSuggestionTargetTotalPagesSelector * @return {number} */ -export const getReciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, +export const getSuggestionTargetTotalPagesSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.totalPages ); /** - * Returns the current page of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetCurrentPageSelector + * Returns the current page of Suggestion Targets. + * @function getSuggestionTargetCurrentPageSelector * @return {number} */ -export const getReciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, +export const getSuggestionTargetCurrentPageSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.currentPage ); /** - * Returns the total number of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetTotalsSelector + * Returns the total number of Suggestion Targets. + * @function getSuggestionTargetTotalsSelector * @return {number} */ -export const getReciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, +export const getSuggestionTargetTotalsSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.totalElements ); /** * Returns Suggestion Targets for the current user. - * @function getCurrentUserReciterSuggestionTargetSelector + * @function getCurrentUserSuggestionTargetSelector * @return {SuggestionTarget[]} */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState, +export const getCurrentUserSuggestionTargetsSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets ); /** * Returns whether or not the user has consulted their suggestions - * @function getCurrentUserReciterSuggestionTargetSelector + * @function getCurrentUserSuggestionTargetSelector * @return {boolean} */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getReciterSuggestionTargetState, +export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited ); diff --git a/src/app/suggestions-page/suggestions-page-routing.module.ts b/src/app/suggestions-page/suggestions-page-routing.module.ts index 05dc6321b7..f7d4ecf955 100644 --- a/src/app/suggestions-page/suggestions-page-routing.module.ts +++ b/src/app/suggestions-page/suggestions-page-routing.module.ts @@ -4,7 +4,7 @@ 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 { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { PublicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; @NgModule({ imports: [ @@ -13,11 +13,11 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso path: ':targetId', resolve: { suggestionTargets: SuggestionsPageResolver, - breadcrumb: I18nBreadcrumbResolver + breadcrumb: PublicationClaimBreadcrumbResolver//I18nBreadcrumbResolver }, data: { - title: 'admin.notifications.recitersuggestion.page.title', - breadcrumbKey: 'admin.notifications.recitersuggestion', + title: 'admin.notifications.publicationclaim.page.title', + breadcrumbKey: 'admin.notifications.publicationclaim', showBreadcrumbsFluid: false }, canActivate: [AuthenticatedGuard], @@ -27,7 +27,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso ]) ], providers: [ - SuggestionsPageResolver + 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 index fb5f08b6a4..4e367b638e 100644 --- a/src/app/suggestions-page/suggestions-page.component.html +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -4,22 +4,21 @@
-

- {{ translateSuggestionType() | translate }} - {{'reciter.suggestion.suggestionFor' | translate}} - {{researcherName}} - {{'reciter.suggestion.from.source' | translate}} {{ translateSuggestionSource() | translate }} -

+

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

- + ({{ getSelectedSuggestionsCount() }}) + (ignoreSuggestionClicked)="ignoreSuggestionAllSelected()">
@@ -33,15 +32,16 @@
  • +
    {{ 'suggestion.count.missing' | translate }}
    diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts index 37d01a4adc..98535297a2 100644 --- a/src/app/suggestions-page/suggestions-page.component.spec.ts +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -1,32 +1,32 @@ import { CommonModule } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +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 { SuggestionListElementComponent } from '../notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; -import { SuggestionsService } from '../notifications/reciter-suggestions/suggestions.service'; +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 { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model'; -import { Suggestion } from '../core/notifications/reciter-suggestions/models/suggestion.model'; -import { mockSuggestionPublicationOne, mockSuggestionPublicationTwo } from '../shared/mocks/reciter-suggestion.mock'; -import { SuggestionEvidencesComponent } from '../notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; +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/reciter-suggestion-targets.mock'; +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 '../notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; +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 { PageInfo } from '../core/shared/page-info.model'; import { TestScheduler } from 'rxjs/testing'; import { getTestScheduler } from 'jasmine-marbles'; import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; @@ -38,7 +38,6 @@ describe('SuggestionPageComponent', () => { let scheduler: TestScheduler; const mockSuggestionsService = getMockSuggestionsService(); const mockSuggestionsTargetStateService = getMockSuggestionNotificationsStateService(); - const suggestionTargetsList: PaginatedList = buildPaginatedList(new PageInfo(), [mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); const router = new RouterStub(); const routeStub = { data: observableOf({ @@ -104,4 +103,112 @@ describe('SuggestionPageComponent', () => { 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 index 72ebc43bc2..fdbf131e59 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -9,25 +9,33 @@ import { SortDirection, SortOptions, } from '../core/cache/models/sort-options.m 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 '../notifications/reciter-suggestions/suggestions.service'; +import { SuggestionBulkResult, SuggestionsService } from '../suggestion-notifications/suggestions.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { Suggestion } from '../core/notifications/reciter-suggestions/models/suggestion.model'; -import { SuggestionTarget } from '../core/notifications/reciter-suggestions/models/suggestion-target.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 '../notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionApproveAndImport } from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; -import { SuggestionTargetsStateService } from '../notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.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 { /** @@ -139,15 +147,6 @@ export class SuggestionsPageComponent implements OnInit { this.processing$.next(false); this.suggestionsRD$.next(results); this.suggestionService.clearSuggestionRequests(); - // navigate to the mydspace if no suggestions remains - - // if (results.totalElements === 0) { - // const content = this.translateService.instant('reciter.suggestion.empty', - // this.suggestionService.getNotificationSuggestionInterpolation(this.suggestionTarget)); - // this.notificationService.success('', content, {timeOut:0}, true); - // TODO if the target is not the current use route to the suggestion target page - // this.router.navigate(['/mydspace']); - // } }); } @@ -155,20 +154,21 @@ export class SuggestionsPageComponent implements OnInit { * Used to delete a suggestion. * @suggestionId */ - notMine(suggestionId) { - this.suggestionService.notMine(suggestionId).subscribe((res) => { + ignoreSuggestion(suggestionId) { + this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); + //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. */ - notMineAllSelected() { + ignoreSuggestionAllSelected() { this.isBulkOperationPending = true; this.suggestionService - .notMineMultiple(Object.values(this.selectedSuggestions)) + .ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) .subscribe((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.updatePage(); @@ -176,12 +176,12 @@ export class SuggestionsPageComponent implements OnInit { this.selectedSuggestions = {}; if (results.success > 0) { this.notificationService.success( - this.translateService.get('reciter.suggestion.notMine.bulk.success', + this.translateService.get('suggestion.ignoreSuggestion.bulk.success', {count: results.success})); } if (results.fails > 0) { this.notificationService.error( - this.translateService.get('reciter.suggestion.notMine.bulk.error', + this.translateService.get('suggestion.ignoreSuggestion.bulk.error', {count: results.fails})); } }); @@ -194,7 +194,7 @@ export class SuggestionsPageComponent implements OnInit { approveAndImport(event: SuggestionApproveAndImport) { this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) .subscribe((workspaceitem: WorkspaceItem) => { - const content = this.translateService.instant('reciter.suggestion.approveAndImport.success', { workspaceItemId: workspaceitem.id }); + const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); this.notificationService.success('', content, {timeOut:0}, true); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); this.updatePage(); @@ -216,12 +216,12 @@ export class SuggestionsPageComponent implements OnInit { this.selectedSuggestions = {}; if (results.success > 0) { this.notificationService.success( - this.translateService.get('reciter.suggestion.approveAndImport.bulk.success', + this.translateService.get('suggestion.approveAndImport.bulk.success', {count: results.success})); } if (results.fails > 0) { this.notificationService.error( - this.translateService.get('reciter.suggestion.approveAndImport.bulk.error', + this.translateService.get('suggestion.approveAndImport.bulk.error', {count: results.fails})); } }); diff --git a/src/app/suggestions-page/suggestions-page.module.ts b/src/app/suggestions-page/suggestions-page.module.ts index f60bcfccaa..e25e483e39 100644 --- a/src/app/suggestions-page/suggestions-page.module.ts +++ b/src/app/suggestions-page/suggestions-page.module.ts @@ -4,17 +4,17 @@ 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 '../notifications/reciter-suggestions/suggestions.service'; +import { SuggestionsService } from '../suggestion-notifications/suggestions.service'; +import { SuggestionsDataService } from '../core/suggestion-notifications/suggestions-data.service'; import { NotificationsModule } from '../notifications/notifications.module'; -import { SuggestionsDataService } from '../core/notifications/reciter-suggestions/suggestions-data.service'; @NgModule({ declarations: [SuggestionsPageComponent], imports: [ CommonModule, SharedModule, - NotificationsModule, - SuggestionsPageRoutingModule + SuggestionsPageRoutingModule, + NotificationsModule ], providers: [ SuggestionsDataService, diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index 87b7559b01..dde5b847f5 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -6,10 +6,10 @@ import { find } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; import { hasValue } from '../shared/empty.util'; -import { SuggestionTarget } from '../core/notifications/reciter-suggestions/models/suggestion-target.model'; +import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model'; import { SuggestionTargetDataService -} from '../core/notifications/reciter-suggestions/target/suggestion-target-data.service'; +} from '../core/suggestion-notifications/target/suggestion-target-data.service'; /** * This class represents a resolver that requests a specific collection before the route is activated 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 326eebe4a7..a4d4a0f2c9 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/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index cff2ac0570..4f9988acb6 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -2190,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", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4b8f831b8d..e20a724158 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1798,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", @@ -2510,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:", @@ -2562,6 +2564,20 @@ "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", @@ -2990,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", @@ -3122,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", @@ -3188,6 +3212,8 @@ "quality-assurance.source.error.service.retrieve": "An error occurred while loading the Quality Assurance source", + "quality-assurance.events.description": "Below the list of all the suggestions for the selected topic.", + "quality-assurance.loading": "Loading ...", "quality-assurance.events.topic": "Topic:", @@ -3306,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", @@ -3592,6 +3620,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", @@ -4150,6 +4244,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 }}", @@ -4274,6 +4380,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", @@ -4366,6 +4474,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", @@ -4388,6 +4498,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", @@ -4444,6 +4560,8 @@ "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 }})", @@ -4468,6 +4586,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", @@ -4516,6 +4636,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", @@ -4578,6 +4700,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.", @@ -5050,6 +5174,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", @@ -5510,332 +5636,7 @@ "vocabulary-treeview.search.form.add": "Add", - "coar-notify-support.title": "COAR Notify Protocol", - - "coar-notify-support-title.content": "Here, we fully support the COAR Notify protocol, which is designed to enhance the communication between repositories. To learn more about the COAR Notify protocol, you can visit their official website here.", - - "coar-notify-support.ldn-inbox.title": "LDN InBox", - - "coar-notify-support.ldn-inbox.content": "For your convenience, our LDN (Linked Data Notifications) InBox is easily accessible at {ldnInboxUrl}. The LDN InBox enables seamless communication and data exchange, ensuring efficient and effective collaboration.", - - "coar-notify-support.message-moderation.title": "Message Moderation", - - "coar-notify-support.message-moderation.content": "To ensure a secure and productive environment, all incoming LDN messages are moderated. If you are planning to exchange information with us, kindly reach out via our dedicated Feedback form. You can access the Feedback form by clicking here.", - - "service.overview.delete.header": "Delete Service", - - "ldn-registered-services.title": "Registered Services", - "ldn-registered-services.table.name": "Name", - "ldn-registered-services.table.description": "Description", - "ldn-registered-services.table.status": "Status", - "ldn-registered-services.table.action": "Action", - "ldn-registered-services.new": "NEW", - "ldn-registered-services.new.breadcrumbs": "Registered Services", - - "ldn-service.overview.table.enabled": "Enabled", - "ldn-service.overview.table.disabled": "Disabled", - "ldn-service.overview.table.clickToEnable": "Click to enable", - "ldn-service.overview.table.clickToDisable": "Click to disable", - - "ldn-edit-registered-service.title": "Edit Service", - "ldn-create-service.title": "Create service", - "service.overview.create.modal": "Create Service", - "service.overview.create.body": "Please confirm the creation of this service.", - "ldn-service-status": "Status", - "service.confirm.create": "Create", - "service.refuse.create": "Cancel", - "ldn-register-new-service.title": "Register a new service", - "ldn-new-service.form.label.submit": "Save", - "ldn-new-service.form.label.name": "Name", - "ldn-new-service.form.label.description": "Description", - "ldn-new-service.form.label.url": "Service URL", - "ldn-new-service.form.label.ip-range": "Service IP range", - "ldn-new-service.form.label.score": "Level of trust", - "ldn-new-service.form.label.ldnUrl": "LDN Inbox URL", - "ldn-new-service.form.placeholder.name": "Please provide service name", - "ldn-new-service.form.placeholder.description": "Please provide a description regarding your service", - "ldn-new-service.form.placeholder.url": "Please input the URL for users to check out more information about the service", - "ldn-new-service.form.placeholder.lowerIp": "IPv4 range lower bound", - "ldn-new-service.form.placeholder.upperIp": "IPv4 range upper bound", - "ldn-new-service.form.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox", - "ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator", - "ldn-service.form.label.placeholder.default-select": "Select a pattern", - - "ldn-service.form.pattern.ack-accept.label": "Acknowledge and Accept", - "ldn-service.form.pattern.ack-accept.description": "This pattern is used to acknowledge and accept a request (offer). It implies an intention to act on the request.", - "ldn-service.form.pattern.ack-accept.category": "Acknowledgements", - - "ldn-service.form.pattern.ack-reject.label": "Acknowledge and Reject", - "ldn-service.form.pattern.ack-reject.description": "This pattern is used to acknowledge and reject a request (offer). It signifies no further action regarding the request.", - "ldn-service.form.pattern.ack-reject.category": "Acknowledgements", - - "ldn-service.form.pattern.ack-tentative-accept.label": "Acknowledge and Tentatively Accept", - "ldn-service.form.pattern.ack-tentative-accept.description": "This pattern is used to acknowledge and tentatively accept a request (offer). It implies an intention to act, which may change.", - "ldn-service.form.pattern.ack-tentative-accept.category": "Acknowledgements", - - "ldn-service.form.pattern.ack-tentative-reject.label": "Acknowledge and Tentatively Reject", - "ldn-service.form.pattern.ack-tentative-reject.description": "This pattern is used to acknowledge and tentatively reject a request (offer). It signifies no further action, subject to change.", - "ldn-service.form.pattern.ack-tentative-reject.category": "Acknowledgements", - - "ldn-service.form.pattern.announce-endorsement.label": "Announce Endorsement", - "ldn-service.form.pattern.announce-endorsement.description": "This pattern is used to announce the existence of an endorsement, referencing the endorsed resource.", - "ldn-service.form.pattern.announce-endorsement.category": "Announcements", - - "ldn-service.form.pattern.announce-ingest.label": "Announce Ingest", - "ldn-service.form.pattern.announce-ingest.description": "This pattern is used to announce that a resource has been ingested.", - "ldn-service.form.pattern.announce-ingest.category": "Announcements", - - "ldn-service.form.pattern.announce-relationship.label": "Announce Relationship", - "ldn-service.form.pattern.announce-relationship.description": "This pattern is used to announce a relationship between two resources.", - "ldn-service.form.pattern.announce-relationship.category": "Announcements", - - "ldn-service.form.pattern.announce-review.label": "Announce Review", - "ldn-service.form.pattern.announce-review.description": "This pattern is used to announce the existence of a review, referencing the reviewed resource.", - "ldn-service.form.pattern.announce-review.category": "Announcements", - - "ldn-service.form.pattern.announce-service-result.label": "Announce Service Result", - "ldn-service.form.pattern.announce-service-result.description": "This pattern is used to announce the existence of a 'service result', referencing the relevant resource.", - "ldn-service.form.pattern.announce-service-result.category": "Announcements", - - "ldn-service.form.pattern.request-endorsement.label": "Request Endorsement", - "ldn-service.form.pattern.request-endorsement.description": "This pattern is used to request endorsement of a resource owned by the origin system.", - "ldn-service.form.pattern.request-endorsement.category": "Requests", - - "ldn-service.form.pattern.request-ingest.label": "Request Ingest", - "ldn-service.form.pattern.request-ingest.description": "This pattern is used to request that the target system ingest a resource.", - "ldn-service.form.pattern.request-ingest.category": "Requests", - - "ldn-service.form.pattern.request-review.label": "Request Review", - "ldn-service.form.pattern.request-review.description": "This pattern is used to request a review of a resource owned by the origin system.", - "ldn-service.form.pattern.request-review.category": "Requests", - - "ldn-service.form.pattern.undo-offer.label": "Undo Offer", - "ldn-service.form.pattern.undo-offer.description": "This pattern is used to undo (retract) an offer previously made.", - "ldn-service.form.pattern.undo-offer.category": "Undo", - - "ldn-new-service.form.label.placeholder.selectedItemFilter": "No Item Filter Selected", - "ldn-new-service.form.label.ItemFilter": "Item Filter", - "ldn-new-service.form.label.automatic": "Automatic", - "ldn-new-service.form.error.name": "Name is required", - "ldn-new-service.form.error.url": "URL is required", - "ldn-new-service.form.error.ipRange": "Please enter a valid IP range", - "ldn-new-service.form.hint.ipRange": "Please enter a valid IpV4 in both range bounds (note: for single IP, please enter the same value in both fields)", - "ldn-new-service.form.error.ldnurl": "LDN URL is required", - "ldn-new-service.form.error.patterns": "At least a pattern is required", - "ldn-new-service.form.error.score": "Please enter a valid score (between 0 and 1). Use the “.” as decimal separator", - - "ldn-new-service.form.label.inboundPattern": "Inbound Pattern", - "ldn-new-service.form.label.addPattern": "+ Add more", - "ldn-new-service.form.label.removeItemFilter": "Remove", - "ldn-register-new-service.breadcrumbs": "New Service", - "service.overview.delete.body": "Are you sure you want to delete this service?", - "service.overview.edit.body": "Do you confirm the changes?", - "service.overview.edit.modal": "Edit Service", - "service.detail.update": "Confirm", - "service.detail.return": "Cancel", - "service.overview.reset-form.body": "Are you sure you want to discard the changes and leave?", - "service.overview.reset-form.modal": "Discard Changes", - "service.overview.reset-form.reset-confirm": "Discard", - "admin.registries.services-formats.modify.success.head": "Successful Edit", - "admin.registries.services-formats.modify.success.content": "The service has been edited", - "admin.registries.services-formats.modify.failure.head": "Failed Edit", - "admin.registries.services-formats.modify.failure.content": "The service has not been edited", - "ldn-service-notification.created.success.title": "Successful Create", - "ldn-service-notification.created.success.body": "The service has been created", - "ldn-service-notification.created.failure.title": "Failed Create", - "ldn-service-notification.created.failure.body": "The service has not been created", - "ldn-service-notification.created.warning.title": "Please select at least one Inbound Pattern", - "ldn-enable-service.notification.success.title": "Successful status updated", - "ldn-enable-service.notification.success.content": "The service status has been updated", - "ldn-service-delete.notification.success.title": "Successful Deletion", - "ldn-service-delete.notification.success.content": "The service has been deleted", - "ldn-service-delete.notification.error.title": "Failed Deletion", - "ldn-service-delete.notification.error.content": "The service has not been deleted", - "service.overview.reset-form.reset-return": "Cancel", - "service.overview.delete": "Delete service", - "ldn-edit-service.title": "Edit service", - "ldn-edit-service.form.label.name": "Name", - "ldn-edit-service.form.label.description": "Description", - "ldn-edit-service.form.label.url": "Service URL", - "ldn-edit-service.form.label.ldnUrl": "LDN Inbox URL", - "ldn-edit-service.form.label.inboundPattern": "Inbound Pattern", - "ldn-edit-service.form.label.noInboundPatternSelected": "No Inbound Pattern", - "ldn-edit-service.form.label.selectedItemFilter": "Selected Item Filter", - "ldn-edit-service.form.label.selectItemFilter": "No Item Filter", - "ldn-edit-service.form.label.automatic": "Automatic", - "ldn-edit-service.form.label.addInboundPattern": "+ Add more", - "ldn-edit-service.form.label.submit": "Save", - "ldn-edit-service.breadcrumbs": "Edit Service", - "ldn-service.control-constaint-select-none": "Select none", - - "ldn-register-new-service.notification.error.title": "Error", - "ldn-register-new-service.notification.error.content": "An error occurred while creating this process", - "ldn-register-new-service.notification.success.title": "Success", - "ldn-register-new-service.notification.success.content": "The process was successfully created", - - "info.coar-notify-support.title": "Notify Support", - - "info.coar-notify.breadcrumbs": "Notify Support", - - "submission.sections.notify.info": "The selected service is compatible with the item according to its current status. {{ service.name }}: {{ service.description }}", - - "item.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", - - "item.qa-event-notification-info.check.button": "Check", - - "mydspace.qa-event-notification.check.notification-info": "There are {{num}} pending review to check", - - "mydspace.qa-event-notification-info.check.button": "Check", - - "item.page.endorsed-by": "Endorsement", - - "item.page.is-reviewed-by": "Review", - - "item.page.is-supplemented-by": "Dataset", - - "item.page.endorsment": "Endorsment", - - "item.page.review": "Review", - - "item.page.dataset": "Dataset", - "menu.section.icon.ldn_services": "LDN Services overview", - "menu.section.services": "LDN Services", - - "menu.section.services_new": "LDN Service", - - "mydspace.import": "Import", - - "mydspace.notification.suggestion": "We found {{count}} publications
    in the {{source}} that seems to be related to your profile.
    Please review the suggestions", - - "mydspace.notification.suggestion.page": "We found {{count}} {{type}} in the {{source}} that seems to be related to your profile. Please review the suggestions.", - - "quality-assurance.topics.description-with-target": "Below you can see all the topics received from the subscriptions to {{source}} in regards to the", - "quality-assurance.events.description": "Below the list of all the suggestions for the selected topic {{topic}}, related to {{source}}.", - - "quality-assurance.events.description-with-topic-and-target": "Below the list of all the suggestions for the selected topic {{topic}}, related to {{source}} and ", - - "quality-assurance.event.table.event.message.serviceUrl": "Service URL:", - - "quality-assurance.event.table.event.message.link": "Link:", - - "service.detail.delete.cancel": "Cancel", - - "service.detail.delete.button": "Delete service", - - "service.detail.delete.header": "Delete service", - - "service.detail.delete.body": "Are you sure you want to delete the current service?", - - "service.detail.delete.confirm": "Delete service", - - "service.detail.delete.success": "The service was successfully deleted.", - - "service.detail.delete.error": "Something went wrong when deleting the service", - - "service.overview.table.id": "Services ID", - - "service.overview.table.name": "Name", - - "service.overview.table.start": "Start time (UTC)", - - "service.overview.table.status": "Status", - - "service.overview.table.user": "User", - - "service.overview.title": "Services Overview", - - "service.overview.breadcrumbs": "Services Overview", - - "service.overview.table.actions": "Actions", - - "service.overview.table.description": "Description", - - "reciter.suggestion.loading": "Loading ...", - - "reciter.suggestion.title": "Suggestions", - - "reciter.suggestion.targets.description": "Below you can see all the suggestions ", - - "reciter.suggestion.targets": "Current Suggestions", - - "reciter.suggestion.table.name": "Researcher Name", - - "reciter.suggestion.table.actions": "Actions", - - "reciter.suggestion.button.review": "Review {{ total }} suggestion(s)", - - "reciter.suggestion.noTargets": "No target found.", - - "reciter.suggestion.target.error.service.retrieve": "An error occurred while loading the Suggestion targets", - - "reciter.suggestion.evidence.type": "Type", - - "reciter.suggestion.evidence.score": "Score", - - "reciter.suggestion.evidence.notes": "Notes", - - "reciter.suggestion.approveAndImport": "Approve & import", - - "reciter.suggestion.approveAndImport.success": "The suggestion has been imported successfully. View.", - - "reciter.suggestion.approveAndImport.bulk": "Approve & import Selected", - - "reciter.suggestion.approveAndImport.bulk.success": "{{ count }} suggestions have been imported successfully ", - - "reciter.suggestion.approveAndImport.bulk.error": "{{ count }} suggestions haven't been imported due to unexpected server errors", - - "reciter.suggestion.notMine": "Not mine", - - "reciter.suggestion.notMine.success": "The suggestion has been discarded", - - "reciter.suggestion.notMine.bulk": "Not mine Selected", - - "reciter.suggestion.notMine.bulk.success": "{{ count }} suggestions have been discarded ", - - "reciter.suggestion.notMine.bulk.error": "{{ count }} suggestions haven't been discarded due to unexpected server errors", - - "reciter.suggestion.seeEvidence": "See evidence", - - "reciter.suggestion.hideEvidence": "Hide evidence", - - "reciter.suggestion.suggestionFor": "Suggestion for", - - "reciter.suggestion.source.oaire": "OpenAIRE Graph", - - "reciter.suggestion.from.source": "from the ", - - "reciter.suggestion.totalScore": "Total Score", - - "reciter.suggestion.type.oaire": "OpenAIRE", - - "submission.sections.submit.progressbar.coarnotify": "COAR Notify", - - "submission.section.section-coar-notify.control.request-review.label": "You can request a review to one of the following services", - - "submission.section.section-coar-notify.control.request-endorsement.label": "You can request an Endorsement to one of the following overlay journals", - - "submission.section.section-coar-notify.control.request-ingest.label": "You can request to ingest a copy of your submission to one of the following services", - - "submission.section.section-coar-notify.dropdown.no-data": "No data available", - - "submission.section.section-coar-notify.dropdown.select-none": "Select none", - - "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", - - "submission.section.section-coar-notify.selection.description": "Selected service's description:", - - "submission.section.section-coar-notify.selection.no-description": "No further information is available", - - "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item. Please check the description for details about which record can be managed by this service.", - - "submission.section.section-coar-notify.info.no-pattern": "No patterns found in the submission.", - - "error.validation.coarnotify.invalidfilter": "Invalid filter, try to select another service or none.", - - "request-status-alert-box.accepted": "The requested {{ offerType }} for {{ serviceName }} has been taken in charge.", - - "request-status-alert-box.rejected": "The requested {{ offerType }} for {{ serviceName }} has been rejected.", - - "request-status-alert-box.requested": "The requested {{ offerType }} for {{ serviceName }} is pending.", + "admin.notifications.publicationclaim.breadcrumbs": "Publication Claim", + "admin.notifications.publicationclaim.page.title": "Publication Claim", } diff --git a/src/assets/images/ror-icon.svg b/src/assets/images/ror-icon.svg new file mode 100644 index 0000000000..24735df519 --- /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 a0d9156415..6c4b99cb0f 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -14,7 +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 './layout-config.interfaces'; +import { SuggestionConfig } from './suggestion-config.interfaces'; import { BundleConfig } from './bundle-config.interface'; import { ActuatorsConfig } from './actuators.config'; import { InfoConfig } from './info-config.interface'; @@ -23,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; diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 902ab68dc3..3b3eb00ddb 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -14,7 +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 './layout-config.interfaces'; +import {SuggestionConfig} from './suggestion-config.interfaces'; import { BundleConfig } from './bundle-config.interface'; import { ActuatorsConfig } from './actuators.config'; import { InfoConfig } from './info-config.interface'; @@ -304,6 +304,9 @@ export class DefaultAppConfig implements AppConfig { // 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 diff --git a/src/config/layout-config.interfaces.ts b/src/config/layout-config.interfaces.ts deleted file mode 100644 index 0b15a06aa9..0000000000 --- a/src/config/layout-config.interfaces.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Config } from './config.interface'; - -export interface UrnConfig extends Config { - name: string; - baseUrl: string; -} - -export interface CrisRefConfig extends Config { - entityType: string; - icon: string; -} - -export interface CrisLayoutMetadataBoxConfig extends Config { - defaultMetadataLabelColStyle: string; - defaultMetadataValueColStyle: string; -} - -export interface CrisLayoutTypeConfig { - orientation: string; -} - -export interface NavbarConfig extends Config { - showCommunityCollection: boolean; -} - -export interface CrisItemPageConfig extends Config { - [entity: string]: CrisLayoutTypeConfig; - default: CrisLayoutTypeConfig; -} - - -export interface CrisLayoutConfig extends Config { - urn: UrnConfig[]; - crisRef: CrisRefConfig[]; - itemPage: CrisItemPageConfig; - metadataBox: CrisLayoutMetadataBoxConfig; -} - -export interface LayoutConfig extends Config { - navbar: NavbarConfig; -} - -export interface SuggestionConfig extends Config { - source: string; - collectionId: string; -} diff --git a/src/config/suggestion-config.interfaces.ts b/src/config/suggestion-config.interfaces.ts new file mode 100644 index 0000000000..afd3a38c58 --- /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 6402209740..76b3d90138 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -323,5 +323,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 fef75b0368..67bee9dac4 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -106,4 +106,7 @@ --ds-comcol-logo-max-width: 500px; --ds-comcol-logo-max-height: 500px; + + --ds-item-page-img-field-default-inline-height: 24px; + } diff --git a/src/themes/custom/eager-theme.module.ts b/src/themes/custom/eager-theme.module.ts index 31047e239a..8889c0c3df 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/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss index 516eff9f7e..8512514a40 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); }