From 5d8d3e35d58f2151e37faff9191159b44c5a33c9 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 29 Mar 2022 16:16:22 +0200 Subject: [PATCH 001/159] [CST-5249] Added suggestions from openaire --- .../admin-notifications-routing-paths.ts | 9 + .../admin-notifications-routing.module.ts | 42 + ...uggestion-targets-page-resolver.service.ts | 32 + ...ons-suggestion-targets-page.component.html | 1 + ...ons-suggestion-targets-page.component.scss | 0 ...-suggestion-targets-page.component.spec.ts | 38 + ...tions-suggestion-targets-page.component.ts | 10 + .../admin-notifications.module.ts | 27 + src/app/admin/admin-routing-paths.ts | 5 + src/app/admin/admin-routing.module.ts | 7 +- .../admin-sidebar/admin-sidebar.component.ts | 38 +- src/app/app-routing.module.ts | 6 + src/app/core/core.module.ts | 6 + ...enaire-suggestion-objects.resource-type.ts | 25 + .../openaire-suggestion-source.model.ts | 47 + .../openaire-suggestion-target.model.ts | 60 + .../models/openaire-suggestion.model.ts | 85 + .../openaire-suggestions-data.service.ts | 301 +++ src/app/openaire/openaire.module.ts | 72 + src/app/openaire/openaire.reducer.ts | 19 + .../openaire/reciter-suggestions/selectors.ts | 97 + .../suggestion-actions.component.html | 28 + .../suggestion-actions.component.scss | 1 + .../suggestion-actions.component.ts | 92 + .../suggestion-evidences.component.html | 20 + .../suggestion-evidences.component.scss | 0 .../suggestion-evidences.component.ts | 15 + .../suggestion-list-element.component.html | 44 + .../suggestion-list-element.component.scss | 16 + .../suggestion-list-element.component.ts | 98 + .../suggestion-targets.actions.ts | 154 ++ .../suggestion-targets.component.html | 51 + .../suggestion-targets.component.scss | 0 .../suggestion-targets.component.ts | 150 ++ .../suggestion-targets.effects.ts | 110 + .../suggestion-targets.reducer.ts | 100 + .../suggestion-targets.state.service.ts | 164 ++ .../suggestions-notification.component.html | 8 + .../suggestions-notification.component.scss | 0 .../suggestions-notification.component.ts | 42 + .../suggestions-popup.component.html | 1 + .../suggestions-popup.component.scss | 0 .../suggestions-popup.component.spec.ts | 79 + .../suggestions-popup.component.ts | 67 + .../suggestions.service.ts | 296 +++ src/app/shared/mocks/openaire.mock.ts | 1797 +++++++++++++++++ .../mocks/reciter-suggestion-targets.mock.ts | 42 + .../shared/mocks/reciter-suggestion.mock.ts | 210 ++ .../suggestions-page-routing-paths.ts | 11 + .../suggestions-page-routing.module.ts | 36 + .../suggestions-page.component.html | 48 + .../suggestions-page.component.scss | 0 .../suggestions-page.component.spec.ts | 107 + .../suggestions-page.component.ts | 285 +++ .../suggestions-page.module.ts | 24 + .../suggestions-page.resolver.ts | 32 + src/assets/i18n/en.json5 | 65 + 57 files changed, 5112 insertions(+), 8 deletions(-) create mode 100644 src/app/admin/admin-notifications/admin-notifications-routing-paths.ts create mode 100644 src/app/admin/admin-notifications/admin-notifications-routing.module.ts create mode 100644 src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service.ts create mode 100644 src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.html create mode 100644 src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.scss create mode 100644 src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.spec.ts create mode 100644 src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.ts create mode 100644 src/app/admin/admin-notifications/admin-notifications.module.ts create mode 100644 src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts create mode 100644 src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts create mode 100644 src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts create mode 100644 src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts create mode 100644 src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts create mode 100644 src/app/openaire/openaire.module.ts create mode 100644 src/app/openaire/openaire.reducer.ts create mode 100644 src/app/openaire/reciter-suggestions/selectors.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.html create mode 100644 src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.scss create mode 100644 src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts create mode 100644 src/app/openaire/reciter-suggestions/suggestions.service.ts create mode 100644 src/app/shared/mocks/openaire.mock.ts create mode 100644 src/app/shared/mocks/reciter-suggestion-targets.mock.ts create mode 100644 src/app/shared/mocks/reciter-suggestion.mock.ts create mode 100644 src/app/suggestions-page/suggestions-page-routing-paths.ts create mode 100644 src/app/suggestions-page/suggestions-page-routing.module.ts create mode 100644 src/app/suggestions-page/suggestions-page.component.html create mode 100644 src/app/suggestions-page/suggestions-page.component.scss create mode 100644 src/app/suggestions-page/suggestions-page.component.spec.ts create mode 100644 src/app/suggestions-page/suggestions-page.component.ts create mode 100644 src/app/suggestions-page/suggestions-page.module.ts create mode 100644 src/app/suggestions-page/suggestions-page.resolver.ts 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..614b2ef49b --- /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 NOTIFICATIONS_EDIT_PATH = 'openaire-broker'; +export const NOTIFICATIONS_RECITER_SUGGESTION_PATH = 'suggestion-targets'; + +export function getNotificationsOpenairebrokerRoute(id: string) { + return new URLCombiner(getNotificationsModuleRoute(), NOTIFICATIONS_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..12bc7b9ec7 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -0,0 +1,42 @@ +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 { NOTIFICATIONS_EDIT_PATH, NOTIFICATIONS_RECITER_SUGGESTION_PATH } from './admin-notifications-routing-paths'; +import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component'; +import { AdminNotificationsSuggestionTargetsPageResolver } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + canActivate: [ AuthenticatedGuard ], + path: `${NOTIFICATIONS_RECITER_SUGGESTION_PATH}`, + component: AdminNotificationsSuggestionTargetsPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: I18nBreadcrumbResolver, + reciterSuggestionTargetParams: AdminNotificationsSuggestionTargetsPageResolver + }, + data: { + title: 'admin.notifications.recitersuggestion.page.title', + breadcrumbKey: 'admin.notifications.recitersuggestion', + showBreadcrumbsFluid: false + } + }, + ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService, + AdminNotificationsSuggestionTargetsPageResolver + ] +}) +/** + * Routing module for the Notifications section of the admin sidebar + */ +export class AdminNotificationsRoutingModule { + +} diff --git a/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service.ts b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service.ts new file mode 100644 index 0000000000..df1f4b81e6 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; + +/** + * Interface for the route parameters. + */ +export interface AdminNotificationsSuggestionTargetsPageParams { + pageId?: string; + pageSize?: number; + currentPage?: number; +} + +/** + * This class represents a resolver that retrieve the route data before the route is activated. + */ +@Injectable() +export class AdminNotificationsSuggestionTargetsPageResolver implements Resolve { + + /** + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsSuggestionTargetsPageParams { + 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-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.html b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.html new file mode 100644 index 0000000000..5d06a1a6bd --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.html @@ -0,0 +1 @@ + diff --git a/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.scss b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.spec.ts b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.spec.ts new file mode 100644 index 0000000000..f9e407f402 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.spec.ts @@ -0,0 +1,38 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('AdminNotificationsSuggestionTargetsPageComponent', () => { + let component: AdminNotificationsSuggestionTargetsPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot() + ], + declarations: [ + AdminNotificationsSuggestionTargetsPageComponent + ], + providers: [ + AdminNotificationsSuggestionTargetsPageComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminNotificationsSuggestionTargetsPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.ts b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.ts new file mode 100644 index 0000000000..a9a77f5089 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-admin-notifications-reciter-page', + templateUrl: './admin-notifications-suggestion-targets-page.component.html', + styleUrls: ['./admin-notifications-suggestion-targets-page.component.scss'] +}) +export class AdminNotificationsSuggestionTargetsPageComponent { + +} 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..47125daad6 --- /dev/null +++ b/src/app/admin/admin-notifications/admin-notifications.module.ts @@ -0,0 +1,27 @@ +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 { OpenaireModule } from '../../openaire/openaire.module'; +import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CoreModule.forRoot(), + AdminNotificationsRoutingModule, + OpenaireModule + ], + declarations: [ + AdminNotificationsSuggestionTargetsPageComponent + ], + entryComponents: [] +}) +/** + * This module handles all components related to the notifications pages + */ +export class AdminNotificationsModule { + +} diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts index 3168ea93c9..30f801cecb 100644 --- a/src/app/admin/admin-routing-paths.ts +++ b/src/app/admin/admin-routing-paths.ts @@ -2,7 +2,12 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAdminModuleRoute } from '../app-routing-paths'; export const REGISTRIES_MODULE_PATH = 'registries'; +export const NOTIFICATIONS_MODULE_PATH = 'notifications'; export function getRegistriesModuleRoute() { return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); } + +export function getNotificationsModuleRoute() { + return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString(); +} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index ee5cb8737b..782a7faa38 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -6,11 +6,16 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; -import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; +import { REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths'; @NgModule({ imports: [ RouterModule.forChild([ + { + path: NOTIFICATIONS_MODULE_PATH, + loadChildren: () => import('./admin-notifications/admin-notifications.module') + .then((m) => m.AdminNotificationsModule), + }, { path: REGISTRIES_MODULE_PATH, loadChildren: () => import('./admin-registries/admin-registries.module') diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index dc9d2a817f..dd5d7e507a 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -22,6 +22,7 @@ import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { Router, ActivatedRoute } from '@angular/router'; +import {NOTIFICATIONS_RECITER_SUGGESTION_PATH} from "../admin-notifications/admin-notifications-routing-paths"; /** * Component representing the admin sidebar @@ -277,7 +278,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { // link: '' // } as LinkMenuItemModel, // icon: 'chart-bar', - // index: 8 + // index: 9 // }, /* Control Panel */ @@ -292,7 +293,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { // link: '' // } as LinkMenuItemModel, // icon: 'cogs', - // index: 9 + // index: 10 // }, /* Processes */ @@ -306,7 +307,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { link: '/processes' } as LinkMenuItemModel, icon: 'terminal', - index: 10 + index: 12 }, ]; menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { @@ -465,6 +466,29 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { createSiteAdministratorMenuSections() { this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { const menuList = [ + /* Notifications */ + { + id: 'notifications', + active: false, + visible: authorized, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.notifications' + } as TextMenuItemModel, + icon: 'bell', + index: 4 + }, + { + id: 'notifications_reciter', + parentID: 'notifications', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.notifications_reciter', + link: '/admin/notifications/' + NOTIFICATIONS_RECITER_SUGGESTION_PATH + } as LinkMenuItemModel, + }, /* Admin Search */ { id: 'admin_search', @@ -476,7 +500,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { link: '/admin/search' } as LinkMenuItemModel, icon: 'search', - index: 5 + index: 6 }, /* Registries */ { @@ -488,7 +512,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { text: 'menu.section.registries' } as TextMenuItemModel, icon: 'list', - index: 6 + index: 7 }, { id: 'registries_metadata', @@ -524,7 +548,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { link: 'admin/curation-tasks' } as LinkMenuItemModel, icon: 'filter', - index: 7 + index: 8 }, /* Workflow */ @@ -601,7 +625,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { text: 'menu.section.access_control' } as TextMenuItemModel, icon: 'key', - index: 4 + index: 5 }, ]; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 88f7791b1b..9bf5518558 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -30,6 +30,7 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; +import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths'; @NgModule({ imports: [ @@ -190,6 +191,11 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard'; .then((m) => m.ProcessPageModule), canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] }, + { path: SUGGESTION_MODULE_PATH, + loadChildren: () => import('./suggestions-page/suggestions-page.module') + .then((m) => m.SuggestionsPageModule), + canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] + }, { path: INFO_MODULE_PATH, loadChildren: () => import('./info/info.module').then((m) => m.InfoModule) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8d8a614a89..6293fbd109 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -162,6 +162,9 @@ import { SearchConfig } from './shared/search/search-filters/search-config.model import { SequenceService } from './shared/sequence.service'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; +import { OpenaireSuggestionTarget } from './openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { OpenaireSuggestion } from './openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { OpenaireSuggestionSource } from './openaire/reciter-suggestions/models/openaire-suggestion-source.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -343,6 +346,9 @@ export const models = ShortLivedToken, Registration, UsageReport, + OpenaireSuggestion, + OpenaireSuggestionTarget, + OpenaireSuggestionSource, Root, SearchConfig, SubmissionAccessesModel diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts new file mode 100644 index 0000000000..e31006959f --- /dev/null +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts @@ -0,0 +1,25 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Suggestion Target object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_TARGET = new ResourceType('suggestiontarget'); + +/** + * The resource type for the Suggestion Source object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_SOURCE = new ResourceType('suggestionsource'); + +/** + * The resource type for the Suggestion object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION = new ResourceType('suggestion'); diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts new file mode 100644 index 0000000000..6da9fd47b9 --- /dev/null +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts @@ -0,0 +1,47 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { CacheableObject } from '../../../cache/object-cache.reducer'; +import { SUGGESTION_SOURCE } from './openaire-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'; + +/** + * The interface representing the Suggestion Source model + */ +@typedObject +export class OpenaireSuggestionSource implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION_SOURCE; + + /** + * The Suggestion Target id + */ + @autoserialize + id: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + total: number; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + suggestiontargets: HALLink + }; +} diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts new file mode 100644 index 0000000000..e35972bc79 --- /dev/null +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts @@ -0,0 +1,60 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { CacheableObject } from '../../../cache/object-cache.reducer'; +import { SUGGESTION_TARGET } from './openaire-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'; + +/** + * The interface representing the Suggestion Target model + */ +@typedObject +export class OpenaireSuggestionTarget implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION_TARGET; + + /** + * The Suggestion Target id + */ + @autoserialize + id: string; + + /** + * The Suggestion Target name to display + */ + @autoserialize + display: string; + + /** + * The Suggestion Target source to display + */ + @autoserialize + source: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + total: number; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + suggestions: HALLink, + target: HALLink + }; +} diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts new file mode 100644 index 0000000000..3ff5d7b630 --- /dev/null +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts @@ -0,0 +1,85 @@ +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; + +import { CacheableObject } from '../../../cache/object-cache.reducer'; +import { SUGGESTION } from './openaire-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'; + +export interface SuggestionEvidences { + [sectionId: string]: { + score: string; + notes: string + }; +} +/** + * The interface representing the Suggestion Source model + */ +@typedObject +export class OpenaireSuggestion implements CacheableObject { + /** + * A string representing the kind of object, e.g. community, item, … + */ + static type = SUGGESTION; + + /** + * The Suggestion id + */ + @autoserialize + id: string; + + /** + * The Suggestion name to display + */ + @autoserialize + display: string; + + /** + * The Suggestion source to display + */ + @autoserialize + source: string; + + /** + * The Suggestion external source uri + */ + @autoserialize + externalSourceUri: string; + + /** + * The Total Score of the suggestion + */ + @autoserialize + score: string; + + /** + * The total number of suggestions provided by Suggestion Target for + */ + @autoserialize + evidences: SuggestionEvidences; + + /** + * All metadata of this suggestion object + */ + @excludeFromEquals + @autoserializeAs(MetadataMapSerializer) + metadata: MetadataMap; + + /** + * The type of this ConfigObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink, + target: HALLink + }; +} diff --git a/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts new file mode 100644 index 0000000000..d961eaf9b5 --- /dev/null +++ b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts @@ -0,0 +1,301 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Store } from '@ngrx/store'; + +import { Observable } from 'rxjs'; + +import { CoreState } from '../../core.reducers'; +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 { FindListOptions } from '../../data/request.models'; +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 { SUGGESTION_TARGET } from './models/openaire-suggestion-objects.resource-type'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { PaginatedList } from '../../data/paginated-list.model'; +import { OpenaireSuggestionSource } from './models/openaire-suggestion-source.model'; +import { OpenaireSuggestionTarget } from './models/openaire-suggestion-target.model'; +import { OpenaireSuggestion } from './models/openaire-suggestion.model'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { NoContent } from '../../shared/NoContent.model'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class SuggestionDataServiceImpl extends DataService { + /** + * The REST endpoint. + */ + protected linkPath = 'suggestions'; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {ChangeAnalyzer} comparator + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } +} + +/** + * A private DataService implementation to delegate specific methods to. + */ +class SuggestionTargetsDataServiceImpl extends DataService { + /** + * The REST endpoint. + */ + protected linkPath = 'suggestiontargets'; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {ChangeAnalyzer} comparator + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } +} + +/** + * A private DataService implementation to delegate specific methods to. + */ +class SuggestionSourcesDataServiceImpl extends DataService { + /** + * The REST endpoint. + */ + protected linkPath = 'suggestionsources'; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {Store} store + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {ChangeAnalyzer} comparator + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } +} + +/** + * The service handling all Suggestion Target REST requests. + */ +@Injectable() +@dataService(SUGGESTION_TARGET) +export class OpenaireSuggestionsDataService { + protected searchFindBySourceMethod = 'findBySource'; + protected searchFindByTargetMethod = 'findByTarget'; + protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; + + /** + * A private DataService implementation to delegate specific methods to. + */ + private suggestionsDataService: SuggestionDataServiceImpl; + + /** + * A private DataService implementation to delegate specific methods to. + */ + private suggestionSourcesDataService: SuggestionSourcesDataServiceImpl; + + /** + * A private DataService implementation to delegate specific methods to. + */ + private suggestionTargetsDataService: SuggestionTargetsDataServiceImpl; + + /** + * Initialize service variables + * @param {RequestService} requestService + * @param {RemoteDataBuildService} rdbService + * @param {ObjectCacheService} objectCache + * @param {HALEndpointService} halService + * @param {NotificationsService} notificationsService + * @param {HttpClient} http + * @param {DefaultChangeAnalyzer} comparatorSuggestions + * @param {DefaultChangeAnalyzer} comparatorSources + * @param {DefaultChangeAnalyzer} comparatorTargets + */ + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparatorSuggestions: DefaultChangeAnalyzer, + protected comparatorSources: DefaultChangeAnalyzer, + protected comparatorTargets: DefaultChangeAnalyzer, + ) { + this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions); + this.suggestionSourcesDataService = new SuggestionSourcesDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources); + this.suggestionTargetsDataService = new SuggestionTargetsDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorTargets); + } + + /** + * Return the list of Suggestion Target + * + * @param options + * Find list options object. + * @return Observable>> + * The list of Suggestion Sources. + */ + public getSources(options: FindListOptions = {}): Observable>> { + return this.suggestionSourcesDataService.findAll(options); + } + + /** + * Return the list of Suggestion Target for a given source + * + * @param source + * The source for which to find targets. + * @param options + * Find list options object. + * @param linksToFollow + * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * @return Observable>> + * The list of Suggestion Target. + */ + public getTargets( + source: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [new RequestParam('source', source)]; + + return this.suggestionTargetsDataService.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.suggestionTargetsDataService.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.suggestionTargetsDataService.findById(targetId); + } + + /** + * Used to delete Suggestion + * @suggestionId + */ + public deleteSuggestion(suggestionId: string): Observable> { + 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 + * + * @param target + * The target for which to find suggestions. + * @param source + * The source for which to find suggestions. + * @param options + * Find list options object. + * @param linksToFollow + * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. + * @return Observable>> + * The list of Suggestion. + */ + public getSuggestionsByTargetAndSource( + target: string, + source: string, + options: FindListOptions = {}, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + options.searchParams = [ + new RequestParam('target', target), + new RequestParam('source', source) + ]; + + return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, true, true, ...linksToFollow); + } + + /** + * Clear findByTargetAndSource suggestions requests from cache + */ + public clearSuggestionRequests() { + this.requestService.setStaleByHrefSubstring(this.searchFindByTargetAndSourceMethod); + } +} diff --git a/src/app/openaire/openaire.module.ts b/src/app/openaire/openaire.module.ts new file mode 100644 index 0000000000..22d04f3002 --- /dev/null +++ b/src/app/openaire/openaire.module.ts @@ -0,0 +1,72 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Action, StoreConfig, StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { CoreModule } from '../core/core.module'; +import { SharedModule } from '../shared/shared.module'; +import { storeModuleConfig } from '../app.reducer'; +import { openaireReducers, OpenaireState } from './openaire.reducer'; +import { SuggestionTargetsStateService } from './reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; +import { SuggestionsService } from './reciter-suggestions/suggestions.service'; +import { OpenaireSuggestionsDataService } from '../core/openaire/reciter-suggestions/openaire-suggestions-data.service'; +import { SuggestionTargetsComponent } from './reciter-suggestions/suggestion-targets/suggestion-targets.component'; +import { SuggestionListElementComponent } from './reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionEvidencesComponent } from './reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; +import { SuggestionActionsComponent } from './reciter-suggestions/suggestion-actions/suggestion-actions.component'; +import { SuggestionsPopupComponent } from './reciter-suggestions/suggestions-popup/suggestions-popup.component'; +import { SuggestionsNotificationComponent } from './reciter-suggestions/suggestions-notification/suggestions-notification.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchModule } from '../shared/search/search.module'; + +const MODULES = [ + CommonModule, + SharedModule, + CoreModule.forRoot(), + StoreModule.forFeature('openaire', openaireReducers, storeModuleConfig as StoreConfig), + TranslateModule +]; + +const COMPONENTS = [ + SuggestionTargetsComponent, + SuggestionActionsComponent, + SuggestionListElementComponent, + SuggestionEvidencesComponent, + SuggestionsPopupComponent, + SuggestionsNotificationComponent +]; + +const DIRECTIVES = [ ]; + + +const PROVIDERS = [ + SuggestionTargetsStateService, + SuggestionsService, + OpenaireSuggestionsDataService +]; + +@NgModule({ + imports: [ + ...MODULES, + SearchModule + ], + declarations: [ + ...COMPONENTS, + ...DIRECTIVES, + ], + providers: [ + ...PROVIDERS + ], + entryComponents: [ + ], + exports: [ + ...COMPONENTS, + ...DIRECTIVES + ] +}) + +/** + * This module handles all components that are necessary for the OpenAIRE components + */ +export class OpenaireModule { +} diff --git a/src/app/openaire/openaire.reducer.ts b/src/app/openaire/openaire.reducer.ts new file mode 100644 index 0000000000..cacf3c7283 --- /dev/null +++ b/src/app/openaire/openaire.reducer.ts @@ -0,0 +1,19 @@ +import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; + +import { + SuggestionTargetsReducer, + SuggestionTargetState +} from './reciter-suggestions/suggestion-targets/suggestion-targets.reducer'; + +/** + * The OpenAIRE State + */ +export interface OpenaireState { + 'suggestionTarget': SuggestionTargetState; +} + +export const openaireReducers: ActionReducerMap = { + suggestionTarget: SuggestionTargetsReducer, +}; + +export const openaireSelector = createFeatureSelector('openaire'); diff --git a/src/app/openaire/reciter-suggestions/selectors.ts b/src/app/openaire/reciter-suggestions/selectors.ts new file mode 100644 index 0000000000..e699b27dba --- /dev/null +++ b/src/app/openaire/reciter-suggestions/selectors.ts @@ -0,0 +1,97 @@ +import { createSelector, MemoizedSelector } from '@ngrx/store'; +import { subStateSelector } from '../../shared/selector.util'; +import { openaireSelector, OpenaireState } from '../openaire.reducer'; +import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; + +/** + * Returns the Reciter Suggestion Target state. + * @function _getReciterSuggestionTargetState + * @param {AppState} state Top level state. + * @return {OpenaireState} + */ +const _getReciterSuggestionTargetState = (state: any) => state.openaire; + +// Reciter Suggestion Targets +// ---------------------------------------------------------------------------- + +/** + * Returns the Reciter Suggestion Targets State. + * @function reciterSuggestionTargetStateSelector + * @return {OpenaireState} + */ +export function reciterSuggestionTargetStateSelector(): MemoizedSelector { + return subStateSelector(openaireSelector, 'suggestionTarget'); +} + +/** + * Returns the Reciter Suggestion Targets list. + * @function reciterSuggestionTargetObjectSelector + * @return {OpenaireReciterSuggestionTarget[]} + */ +export function reciterSuggestionTargetObjectSelector(): MemoizedSelector { + return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); +} + +/** + * Returns true if the Reciter Suggestion Targets are loaded. + * @function isReciterSuggestionTargetLoadedSelector + * @return {boolean} + */ +export const isReciterSuggestionTargetLoadedSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.loaded +); + +/** + * Returns true if the deduplication sets are processing. + * @function isDeduplicationSetsProcessingSelector + * @return {boolean} + */ +export const isreciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.processing +); + +/** + * Returns the total available pages of Reciter Suggestion Targets. + * @function getreciterSuggestionTargetTotalPagesSelector + * @return {number} + */ +export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.totalPages +); + +/** + * Returns the current page of Reciter Suggestion Targets. + * @function getreciterSuggestionTargetCurrentPageSelector + * @return {number} + */ +export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.currentPage +); + +/** + * Returns the total number of Reciter Suggestion Targets. + * @function getreciterSuggestionTargetTotalsSelector + * @return {number} + */ +export const getreciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.totalElements +); + +/** + * Returns Suggestion Targets for the current user. + * @function getCurrentUserReciterSuggestionTargetSelector + * @return {OpenaireSuggestionTarget[]} + */ +export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.currentUserTargets +); + +/** + * Returns whether or not the user has consulted their suggestions + * @function getCurrentUserReciterSuggestionTargetSelector + * @return {boolean} + */ +export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getReciterSuggestionTargetState, + (state: OpenaireState) => state.suggestionTarget.currentUserTargetsVisited +); diff --git a/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.html b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.html new file mode 100644 index 0000000000..7ec3e61395 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.html @@ -0,0 +1,28 @@ +
+
+ + + + + + + +
+ + +
diff --git a/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts new file mode 100644 index 0000000000..cf21901123 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts @@ -0,0 +1,92 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { OpenaireSuggestion } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-list-element.component'; +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'; + +@Component({ + selector: 'ds-suggestion-actions', + styleUrls: [ './suggestion-actions.component.scss' ], + templateUrl: './suggestion-actions.component.html' +}) +export class SuggestionActionsComponent { + + @Input() object: OpenaireSuggestion; + + @Input() isBulk = false; + + @Input() hasEvidence = false; + + @Input() seeEvidence = false; + + @Input() isCollectionFixed = false; + + /** + * The component is used to Delete suggestion + */ + @Output() notMineClicked = new EventEmitter(); + + /** + * The component is used to approve & import + */ + @Output() approveAndImport = new EventEmitter(); + + /** + * The component is used to approve & import + */ + @Output() seeEvidences = new EventEmitter(); + + constructor(private modalService: NgbModal) { } + + /** + * Method called on clicking the button "approve & import", It opens a dialog for + * select a collection and it emits an approveAndImport event. + */ + openDialog(entity: ItemType) { + + const modalRef = this.modalService.open(CreateItemParentSelectorComponent); + modalRef.componentInstance.emitOnly = true; + modalRef.componentInstance.entityType = entity.label; + + modalRef.componentInstance.select.pipe(take(1)) + .subscribe((collection: Collection) => { + this.approveAndImport.emit({ + suggestion: this.isBulk ? undefined : this.object, + collectionId: collection.id + }); + }); + } + + approveAndImportCollectionFixed() { + this.approveAndImport.emit({ + suggestion: this.isBulk ? undefined : this.object, + collectionId: null + }); + } + + + /** + * Delete the suggestion + */ + notMine() { + this.notMineClicked.emit(this.isBulk ? undefined : this.object.id); + } + + /** + * Toggle See Evidence + */ + toggleSeeEvidences() { + this.seeEvidences.emit(!this.seeEvidence); + } + + notMineLabel(): string { + return this.isBulk ? 'reciter.suggestion.notMine.bulk' : 'reciter.suggestion.notMine' ; + } + + approveAndImportLabel(): string { + return this.isBulk ? 'reciter.suggestion.approveAndImport.bulk' : 'reciter.suggestion.approveAndImport'; + } +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html new file mode 100644 index 0000000000..5ad4f0a978 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.html @@ -0,0 +1,20 @@ +
+
+ + + + + + + + + + + + + + + +
{{'reciter.suggestion.evidence.score' | translate}}{{'reciter.suggestion.evidence.type' | translate}}{{'reciter.suggestion.evidence.notes' | translate}}
{{evidences[evidence].score}}{{evidence | translate}}{{evidences[evidence].notes}}
+
+
diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts new file mode 100644 index 0000000000..e0536ae723 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { fadeIn } from '../../../../shared/animations/fade'; +import { SuggestionEvidences } from '../../../../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; + +@Component({ + selector: 'ds-suggestion-evidences', + styleUrls: [ './suggestion-evidences.component.scss' ], + templateUrl: './suggestion-evidences.component.html', + animations: [fadeIn] +}) +export class SuggestionEvidencesComponent { + + @Input() evidences: SuggestionEvidences; + +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html new file mode 100644 index 0000000000..05f9c0ac77 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html @@ -0,0 +1,44 @@ +
+
+ +
+
+ +
+
+ +
+
+
{{'reciter.suggestion.totalScore' | translate}}
+ {{ object.score }} +
+
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss new file mode 100644 index 0000000000..1c52209518 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.scss @@ -0,0 +1,16 @@ +.issue-date { + color: #c8c8c8; +} + +.parent { + display: flex; + gap:10px; +} + +.import { + flex: initial; +} + +.suggestion-score { + font-size: 1.5rem; +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts new file mode 100644 index 0000000000..8227dc3213 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts @@ -0,0 +1,98 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { fadeIn } from '../../../shared/animations/fade'; +import { OpenaireSuggestion } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { Item } from '../../../core/shared/item.model'; +import { isNotEmpty } from '../../../shared/empty.util'; + +export interface SuggestionApproveAndImport { + suggestion: OpenaireSuggestion; + collectionId: string; +} + +@Component({ + selector: 'ds-suggestion-list-item', + styleUrls: ['./suggestion-list-element.component.scss'], + templateUrl: './suggestion-list-element.component.html', + animations: [fadeIn] +}) +export class SuggestionListElementComponent implements OnInit { + + @Input() object: OpenaireSuggestion; + + @Input() isSelected = false; + + @Input() isCollectionFixed = false; + + public listableObject: any; + + public seeEvidence = false; + + /** + * The component is used to Delete suggestion + */ + @Output() notMineClicked = new EventEmitter(); + + /** + * The component is used to approve & import + */ + @Output() approveAndImport = new EventEmitter(); + + /** + * New value whether the element is selected + */ + @Output() selected = new EventEmitter(); + + /** + * Initialize instance variables + * + * @param {NgbModal} modalService + */ + constructor(private modalService: NgbModal) { } + + ngOnInit() { + this.listableObject = { + indexableObject: Object.assign(new Item(), {id: this.object.id, metadata: this.object.metadata}), + hitHighlights: {} + }; + } + + /** + * Approve and import the suggestion + */ + onApproveAndImport(event: SuggestionApproveAndImport) { + this.approveAndImport.emit(event); + } + + /** + * Delete the suggestion + */ + onNotMine(suggestionId: string) { + this.notMineClicked.emit(suggestionId); + } + + /** + * Change is selected value. + */ + changeSelected(event) { + this.isSelected = event.target.checked; + this.selected.next(this.isSelected); + } + + /** + * See the Evidence + */ + hasEvidences() { + return isNotEmpty(this.object.evidences); + } + + /** + * Set the see evidence variable. + */ + onSeeEvidences(seeEvidence: boolean) { + this.seeEvidence = seeEvidence; + } + +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts new file mode 100644 index 0000000000..6c44d40b91 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts @@ -0,0 +1,154 @@ +import { Action } from '@ngrx/store'; +import { type } from '../../../shared/ngrx/type'; +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const SuggestionTargetActionTypes = { + ADD_TARGETS: type('dspace/integration/openaire/suggestions/target/ADD_TARGETS'), + CLEAR_TARGETS: type('dspace/integration/openaire/suggestions/target/CLEAR_TARGETS'), + RETRIEVE_TARGETS_BY_SOURCE: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE'), + RETRIEVE_TARGETS_BY_SOURCE_ERROR: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE_ERROR'), + ADD_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/ADD_USER_SUGGESTIONS'), + REFRESH_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/REFRESH_USER_SUGGESTIONS'), + MARK_USER_SUGGESTIONS_AS_VISITED: type('dspace/integration/openaire/suggestions/target/MARK_USER_SUGGESTIONS_AS_VISITED') +}; + +/* tslint:disable:max-classes-per-file */ + +/** + * An ngrx action to retrieve all the Suggestion Targets. + */ +export class RetrieveTargetsBySourceAction implements Action { + type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; + payload: { + source: string; + elementsPerPage: number; + currentPage: number; + }; + + /** + * Create a new RetrieveTargetsBySourceAction. + * + * @param source + * the source for which to retrieve suggestion targets + * @param elementsPerPage + * the number of targets per page + * @param currentPage + * The page number to retrieve + */ + constructor(source: string, elementsPerPage: number, currentPage: number) { + this.payload = { + source, + elementsPerPage, + currentPage + }; + } +} + +/** + * An ngrx action for retrieving 'all Suggestion Targets' error. + */ +export class RetrieveAllTargetsErrorAction implements Action { + type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR; +} + +/** + * An ngrx action to load the Suggestion Target objects. + */ +export class AddTargetAction implements Action { + type = SuggestionTargetActionTypes.ADD_TARGETS; + payload: { + targets: OpenaireSuggestionTarget[]; + totalPages: number; + currentPage: number; + totalElements: number; + }; + + /** + * Create a new AddTargetAction. + * + * @param targets + * the list of targets + * @param totalPages + * the total available pages of targets + * @param currentPage + * the current page + * @param totalElements + * the total available Suggestion Targets + */ + constructor(targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + this.payload = { + targets, + totalPages, + currentPage, + totalElements + }; + } + +} + +/** + * An ngrx action to load the user Suggestion Target object. + * Called by the ??? effect. + */ +export class AddUserSuggestionsAction implements Action { + type = SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS; + payload: { + suggestionTargets: OpenaireSuggestionTarget[]; + }; + + /** + * Create a new AddUserSuggestionsAction. + * + * @param suggestionTargets + * the user suggestions target + */ + constructor(suggestionTargets: OpenaireSuggestionTarget[]) { + this.payload = { suggestionTargets }; + } + +} + +/** + * An ngrx action to reload the user Suggestion Target object. + * Called by the ??? effect. + */ +export class RefreshUserSuggestionsAction implements Action { + type = SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS; +} + +/** + * An ngrx action to Mark User Suggestions As Visited. + * Called by the ??? effect. + */ +export class MarkUserSuggestionsAsVisitedAction implements Action { + type = SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED; +} + +/** + * An ngrx action to clear targets state. + */ +export class ClearSuggestionTargetsAction implements Action { + type = SuggestionTargetActionTypes.CLEAR_TARGETS; +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types. + */ +export type SuggestionTargetsActions + = AddTargetAction + | AddUserSuggestionsAction + | ClearSuggestionTargetsAction + | MarkUserSuggestionsAsVisitedAction + | RetrieveTargetsBySourceAction + | RetrieveAllTargetsErrorAction; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html new file mode 100644 index 0000000000..791e694ba9 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html @@ -0,0 +1,51 @@ +
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + +
{{'reciter.suggestion.table.name' | translate}}{{'reciter.suggestion.table.actions' | translate}}
+ {{targetElement.display}} + +
+ +
+
+
+
+
+
+
+
diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.scss b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts new file mode 100644 index 0000000000..b9ed6c4e87 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts @@ -0,0 +1,150 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, take } from 'rxjs/operators'; + +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { hasValue } from '../../../shared/empty.util'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SuggestionTargetsStateService } from './suggestion-targets.state.service'; +import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths'; +import { SuggestionsService } from '../suggestions.service'; +import { PaginationService } from '../../../core/pagination/pagination.service'; + +/** + * Component to display the Suggestion Target list. + */ +@Component({ + selector: 'ds-suggestion-target', + templateUrl: './suggestion-targets.component.html', + styleUrls: ['./suggestion-targets.component.scss'], +}) +export class SuggestionTargetsComponent implements OnInit { + + /** + * The source for which to list targets + */ + @Input() source: string; + + /** + * The pagination system configuration for HTML listing. + * @type {PaginationComponentOptions} + */ + public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'stp', + pageSizeOptions: [5, 10, 20, 40, 60] + }); + + /** + * The Suggestion Target list. + */ + public targets$: Observable; + /** + * The total number of Suggestion Targets. + */ + public totalElements$: Observable; + /** + * Array to track all the component subscriptions. Useful to unsubscribe them with 'onDestroy'. + * @type {Array} + */ + protected subs: Subscription[] = []; + + /** + * Initialize the component variables. + * @param {PaginationService} paginationService + * @param {SuggestionTargetsStateService} suggestionTargetsStateService + * @param {SuggestionsService} suggestionService + * @param {Router} router + */ + constructor( + private paginationService: PaginationService, + private suggestionTargetsStateService: SuggestionTargetsStateService, + private suggestionService: SuggestionsService, + private router: Router + ) { + } + + /** + * Component initialization. + */ + ngOnInit(): void { + this.targets$ = this.suggestionTargetsStateService.getReciterSuggestionTargets(); + this.totalElements$ = this.suggestionTargetsStateService.getReciterSuggestionTargetsTotals(); + } + + /** + * First Suggestion Targets loading after view initialization. + */ + ngAfterViewInit(): void { + this.subs.push( + this.suggestionTargetsStateService.isReciterSuggestionTargetsLoaded().pipe( + take(1) + ).subscribe(() => { + this.getSuggestionTargets(); + }) + ); + } + + /** + * Returns the information about the loading status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if the targets are loading, 'false' otherwise. + */ + public isTargetsLoading(): Observable { + return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(); + } + + /** + * Returns the information about the processing status of the Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. + */ + public isTargetsProcessing(): Observable { + return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(); + } + + /** + * Redirect to suggestion page. + * + * @param {string} id + * the id of suggestion target + * @param {string} name + * the name of suggestion target + */ + public redirectToSuggestions(id: string, name: string) { + this.router.navigate([getSuggestionPageRoute(id)]); + } + + /** + * Unsubscribe from all subscriptions. + */ + ngOnDestroy(): void { + this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + + /** + * Dispatch the Suggestion Targets retrival. + */ + public getSuggestionTargets(): void { + this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe( + distinctUntilChanged(), + take(1) + ).subscribe((options: PaginationComponentOptions) => { + this.suggestionTargetsStateService.dispatchRetrieveReciterSuggestionTargets( + this.source, + options.pageSize, + options.currentPage + ); + }); + } + + public getTargetUuid(target: OpenaireSuggestionTarget) { + return this.suggestionService.getTargetUuid(target); + } +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts new file mode 100644 index 0000000000..85e871403c --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -0,0 +1,110 @@ +import { Injectable } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { TranslateService } from '@ngx-translate/core'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import { + AddTargetAction, + AddUserSuggestionsAction, + RefreshUserSuggestionsAction, + RetrieveAllTargetsErrorAction, + RetrieveTargetsBySourceAction, + SuggestionTargetActionTypes, +} from './suggestion-targets.actions'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { SuggestionsService } from '../suggestions.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { AuthActionTypes, RetrieveAuthenticatedEpersonSuccessAction } from '../../../core/auth/auth.actions'; +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; + +/** + * Provides effect methods for the Suggestion Targets actions. + */ +@Injectable() +export class SuggestionTargetsEffects { + + /** + * Retrieve all Suggestion Targets managing pagination and errors. + */ + @Effect() retrieveTargetsBySource$ = this.actions$.pipe( + ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE), + switchMap((action: RetrieveTargetsBySourceAction) => { + return this.suggestionsService.getTargets( + action.payload.source, + action.payload.elementsPerPage, + action.payload.currentPage + ).pipe( + map((targets: PaginatedList) => + new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements) + ), + catchError((error: Error) => { + if (error) { + console.error(error.message); + } + return of(new RetrieveAllTargetsErrorAction()); + }) + ); + }) + ); + + /** + * Show a notification on error. + */ + @Effect({ dispatch: false }) retrieveAllTargetsErrorAction$ = this.actions$.pipe( + ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR), + tap(() => { + this.notificationsService.error(null, this.translate.get('reciter.suggestion.target.error.service.retrieve')); + }) + ); + + /** + * Show a notification on error. + */ + @Effect() retrieveUserTargets$ = this.actions$.pipe( + ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS), + switchMap((action: RetrieveAuthenticatedEpersonSuccessAction) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(action.payload).pipe( + map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)) + ); + })); + + /** + * Fetch the current user suggestion + */ + @Effect() refreshUserTargets$ = this.actions$.pipe( + ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), + switchMap((action: RefreshUserSuggestionsAction) => { + return this.store$.select((state: any) => state.core.auth.user) + .pipe( + switchMap((user: EPerson) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(user) + .pipe( + map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), + catchError((errors) => of(errors)) + ); + }), + catchError((errors) => of(errors)) + ); + })); + + /** + * Initialize the effect class variables. + * @param {Actions} actions$ + * @param {Store} store$ + * @param {TranslateService} translate + * @param {NotificationsService} notificationsService + * @param {SuggestionsService} suggestionsService + */ + constructor( + private actions$: Actions, + private store$: Store, + private translate: TranslateService, + private notificationsService: NotificationsService, + private suggestionsService: SuggestionsService + ) { + } +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts new file mode 100644 index 0000000000..f8bd53ec05 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts @@ -0,0 +1,100 @@ +import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; + +/** + * The interface representing the OpenAIRE suggestion targets state. + */ +export interface SuggestionTargetState { + targets: OpenaireSuggestionTarget[]; + processing: boolean; + loaded: boolean; + totalPages: number; + currentPage: number; + totalElements: number; + currentUserTargets: OpenaireSuggestionTarget[]; + currentUserTargetsVisited: boolean; +} + +/** + * Used for the OpenAIRE Suggestion Target state initialization. + */ +const SuggestionTargetInitialState: SuggestionTargetState = { + targets: [], + processing: false, + loaded: false, + totalPages: 0, + currentPage: 0, + totalElements: 0, + currentUserTargets: null, + currentUserTargetsVisited: false +}; + +/** + * The OpenAIRE Broker Topic Reducer + * + * @param state + * the current state initialized with SuggestionTargetInitialState + * @param action + * the action to perform on the state + * @return SuggestionTargetState + * the new state + */ +export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { + switch (action.type) { + case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { + return Object.assign({}, state, { + targets: [], + processing: true + }); + } + + case SuggestionTargetActionTypes.ADD_TARGETS: { + return Object.assign({}, state, { + targets: state.targets.concat(action.payload.targets), + processing: false, + loaded: true, + totalPages: action.payload.totalPages, + currentPage: state.currentPage, + totalElements: action.payload.totalElements + }); + } + + case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { + return Object.assign({}, state, { + targets: [], + processing: false, + loaded: true, + totalPages: 0, + currentPage: 0, + totalElements: 0, + }); + } + + case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { + return Object.assign({}, state, { + currentUserTargets: action.payload.suggestionTargets + }); + } + + case SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED: { + return Object.assign({}, state, { + currentUserTargetsVisited: true + }); + } + + case SuggestionTargetActionTypes.CLEAR_TARGETS: { + return Object.assign({}, state, { + targets: [], + processing: false, + loaded: false, + totalPages: 0, + currentPage: 0, + totalElements: 0, + }); + } + + default: { + return state; + } + } +} diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts new file mode 100644 index 0000000000..2e05bce0a9 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from '@angular/core'; + +import { select, Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { + getCurrentUserSuggestionTargetsSelector, + getCurrentUserSuggestionTargetsVisitedSelector, + getreciterSuggestionTargetCurrentPageSelector, + getreciterSuggestionTargetTotalsSelector, + isReciterSuggestionTargetLoadedSelector, + isreciterSuggestionTargetProcessingSelector, + reciterSuggestionTargetObjectSelector +} from '../selectors'; +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + ClearSuggestionTargetsAction, + MarkUserSuggestionsAsVisitedAction, + RefreshUserSuggestionsAction, + RetrieveTargetsBySourceAction +} from './suggestion-targets.actions'; +import { OpenaireState } from '../../openaire.reducer'; + +/** + * The service handling the Suggestion targets State. + */ +@Injectable() +export class SuggestionTargetsStateService { + + /** + * Initialize the service variables. + * @param {Store} store + */ + constructor(private store: Store) { } + + /** + * Returns the list of Reciter Suggestion Targets from the state. + * + * @return Observable + * The list of Reciter Suggestion Targets. + */ + public getReciterSuggestionTargets(): Observable { + return this.store.pipe(select(reciterSuggestionTargetObjectSelector())); + } + + /** + * Returns the information about the loading status of the Reciter Suggestion Targets (if it's running or not). + * + * @return Observable + * 'true' if the targets are loading, 'false' otherwise. + */ + public isReciterSuggestionTargetsLoading(): Observable { + return this.store.pipe( + select(isReciterSuggestionTargetLoadedSelector), + map((loaded: boolean) => !loaded) + ); + } + + /** + * Returns the information about the loading status of the Reciter 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)); + } + + /** + * Returns the information about the processing status of the Reciter 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 { + return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector)); + } + + /** + * Returns, from the state, the total available pages of the Reciter Suggestion Targets. + * + * @return Observable + * The number of the Reciter Suggestion Targets pages. + */ + public getReciterSuggestionTargetsTotalPages(): Observable { + return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + } + + /** + * Returns the current page of the Reciter Suggestion Targets, from the state. + * + * @return Observable + * The number of the current Reciter Suggestion Targets page. + */ + public getReciterSuggestionTargetsCurrentPage(): Observable { + return this.store.pipe(select(getreciterSuggestionTargetCurrentPageSelector)); + } + + /** + * Returns the total number of the Reciter Suggestion Targets. + * + * @return Observable + * The number of the Reciter Suggestion Targets. + */ + public getReciterSuggestionTargetsTotals(): Observable { + return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + } + + /** + * Dispatch a request to change the Reciter Suggestion Targets state, retrieving the targets from the server. + * + * @param source + * the source for which to retrieve suggestion targets + * @param elementsPerPage + * The number of the targets per page. + * @param currentPage + * The number of the current page. + */ + public dispatchRetrieveReciterSuggestionTargets(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. + * + * @return Observable + * The Reciter Suggestion Targets object. + */ + public getCurrentUserSuggestionTargets(): Observable { + return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); + } + + /** + * Returns, from the state, whether or not the user has consulted their suggestion targets. + * + * @return Observable + * True if user already visited, false otherwise. + */ + public hasUserVisitedSuggestions(): Observable { + return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector)); + } + + /** + * Dispatch a new MarkUserSuggestionsAsVisitedAction + */ + public dispatchMarkUserSuggestionsAsVisitedAction(): void { + this.store.dispatch(new MarkUserSuggestionsAsVisitedAction()); + } + + /** + * Dispatch an action to clear the Reciter Suggestion Targets state. + */ + public dispatchClearSuggestionTargetsAction(): void { + this.store.dispatch(new ClearSuggestionTargetsAction()); + } + + /** + * Dispatch an action to refresh the user suggestions. + */ + public dispatchRefreshUserSuggestionsAction(): void { + this.store.dispatch(new RefreshUserSuggestionsAction()); + } +} diff --git a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.html b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.html new file mode 100644 index 0000000000..577aa496b3 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.html @@ -0,0 +1,8 @@ + + +
+
+
+
+
+
diff --git a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.scss b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts new file mode 100644 index 0000000000..094dfab017 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +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 { Observable } from 'rxjs'; + +@Component({ + selector: 'ds-suggestions-notification', + templateUrl: './suggestions-notification.component.html', + styleUrls: ['./suggestions-notification.component.scss'] +}) +export class SuggestionsNotificationComponent implements OnInit { + + labelPrefix = 'mydspace.'; + + /** + * The user suggestion targets. + */ + suggestionsRD$: Observable; + + constructor( + private translateService: TranslateService, + private reciterSuggestionStateService: SuggestionTargetsStateService, + private notificationsService: NotificationsService, + private suggestionsService: SuggestionsService + ) { } + + ngOnInit() { + this.suggestionsRD$ = this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(); + } + + /** + * Interpolated params to build the notification suggestions notification. + * @param suggestionTarget + */ + public getNotificationSuggestionInterpolation(suggestionTarget: OpenaireSuggestionTarget): any { + return this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget); + } + +} diff --git a/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.html b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.html new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.html @@ -0,0 +1 @@ + diff --git a/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.scss b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts new file mode 100644 index 0000000000..67678354ca --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.spec.ts @@ -0,0 +1,79 @@ +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 { SuggestionsService } from '../suggestions.service'; + +describe('SuggestionsPopupComponent', () => { + let component: SuggestionsPopupComponent; + let fixture: ComponentFixture; + + const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', { + hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'), + getCurrentUserSuggestionTargets: jasmine.createSpy('getCurrentUserSuggestionTargets'), + dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction') + }); + + const mockNotificationInterpolation = { count: 12, source: 'source', suggestionId: 'id', displayName: 'displayName' }; + const suggestionService = jasmine.createSpyObj('SuggestionService', { + getNotificationSuggestionInterpolation: + jasmine.createSpy('getNotificationSuggestionInterpolation').and.returnValue(mockNotificationInterpolation) + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ SuggestionsPopupComponent ], + providers: [ + { provide: SuggestionTargetsStateService, useValue: suggestionStateService }, + { provide: SuggestionsService, useValue: suggestionService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA] + + }) + .compileComponents(); + })); + + describe('should create', () => { + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionsPopupComponent); + component = fixture.componentInstance; + spyOn(component, 'initializePopup').and.returnValue(null); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(component.initializePopup).toHaveBeenCalled(); + }); + + }); + + describe('when there are publication suggestions', () => { + + beforeEach(() => { + + suggestionStateService.hasUserVisitedSuggestions.and.returnValue(observableOf(false)); + suggestionStateService.getCurrentUserSuggestionTargets.and.returnValue(observableOf([mockSuggestionTargetsObjectOne])); + suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction.and.returnValue(observableOf(null)); + + fixture = TestBed.createComponent(SuggestionsPopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should show a notification when new publication suggestions are available', () => { + expect((component as any).notificationsService.success).toHaveBeenCalled(); + expect(suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction).toHaveBeenCalled(); + }); + + }); +}); diff --git a/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts new file mode 100644 index 0000000000..6135cd99ea --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts @@ -0,0 +1,67 @@ +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 { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-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() { + 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: OpenaireSuggestionTarget) => this.showNotificationForNewSuggestions(suggestionTarget)); + this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction(); + notifier.next(); + notifier.complete(); + } + } + }); + } + + /** + * Show a notification to user for a new suggestions detected + * @param suggestionTarget + * @private + */ + private showNotificationForNewSuggestions(suggestionTarget: OpenaireSuggestionTarget): 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/openaire/reciter-suggestions/suggestions.service.ts b/src/app/openaire/reciter-suggestions/suggestions.service.ts new file mode 100644 index 0000000000..de743d95e3 --- /dev/null +++ b/src/app/openaire/reciter-suggestions/suggestions.service.ts @@ -0,0 +1,296 @@ +import { Injectable } from '@angular/core'; + +import { of, forkJoin, Observable } from 'rxjs'; +import { catchError, map, mergeMap, take } from 'rxjs/operators'; + +import { OpenaireSuggestionsDataService } from '../../core/openaire/reciter-suggestions/openaire-suggestions-data.service'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; +import { + getAllSucceededRemoteDataPayload, + getFinishedRemoteData, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload +} from '../../core/shared/operators'; +import { OpenaireSuggestion } from '../../core/openaire/reciter-suggestions/models/openaire-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 { SuggestionConfig } from '../../../config/layout-config.interfaces'; +import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; + +export interface SuggestionBulkResult { + success: number; + fails: number; +} + +/** + * The service handling all Suggestion Target requests to the REST service. + */ +@Injectable() +export class SuggestionsService { + + /** + * Initialize the service variables. + * @param {AuthService} authService + * @param {ResearcherProfileService} researcherProfileService + * @param {OpenaireSuggestionsDataService} suggestionsDataService + */ + constructor( + private authService: AuthService, + private researcherProfileService: ResearcherProfileService, + private suggestionsDataService: OpenaireSuggestionsDataService, + private translateService: TranslateService + ) { + } + + /** + * Return the list of Suggestion Target managing pagination and errors. + * + * @param source + * The source for which to retrieve targets + * @param elementsPerPage + * The number of the target per page + * @param currentPage + * The page number to retrieve + * @return Observable> + * The list of Suggestion Targets. + */ + public getTargets(source, elementsPerPage, currentPage): Observable> { + const sortOptions = new SortOptions('display', SortDirection.ASC); + + const findListOptions: FindListOptions = { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + sort: sortOptions + }; + + return this.suggestionsDataService.getTargets(source, findListOptions).pipe( + getFinishedRemoteData(), + take(1), + map((rd: RemoteData>) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + throw new Error('Can\'t retrieve Suggestion Target from the Search Target REST service'); + } + }) + ); + } + + /** + * Return the list of review suggestions Target managing pagination and errors. + * + * @param targetId + * The target id for which to find suggestions. + * @param elementsPerPage + * The number of the target per page + * @param currentPage + * The page number to retrieve + * @param sortOptions + * The sort options + * @return Observable>> + * The list of Suggestion. + */ + public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable> { + const [source, target] = targetId.split(':'); + + const findListOptions: FindListOptions = { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + sort: sortOptions + }; + + return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions).pipe( + getAllSucceededRemoteDataPayload() + ); + } + + /** + * Clear suggestions requests from cache + */ + public clearSuggestionRequests() { + this.suggestionsDataService.clearSuggestionRequests(); + } + + /** + * Used to delete Suggestion + * @suggestionId + */ + public deleteReviewedSuggestion(suggestionId: string): Observable> { + return this.suggestionsDataService.deleteSuggestion(suggestionId).pipe( + map((response: RemoteData) => { + if (response.isSuccess) { + return response; + } else { + throw new Error('Can\'t delete Suggestion from the Search Target REST service'); + } + }), + take(1) + ); + } + + /** + * Retrieve suggestion targets for the given user + * + * @param user + * The EPerson object for which to retrieve suggestion targets + */ + public retrieveCurrentUserSuggestions(user: EPerson): Observable { + return this.researcherProfileService.findById(user.uuid).pipe( + mergeMap((profile: ResearcherProfile) => { + if (isNotEmpty(profile)) { + return this.researcherProfileService.findRelatedItemId(profile).pipe( + mergeMap((itemId: string) => { + return this.suggestionsDataService.getTargetsByUser(itemId).pipe( + getFirstSucceededRemoteListPayload() + ); + }) + ); + } else { + return of([]); + } + }), + take(1) + ); + } + + /** + * Perform the approve and import operation over a single suggestion + * @param suggestion target suggestion + * @param collectionId the collectionId + * @param workspaceitemService injected dependency + * @private + */ + public approveAndImport(workspaceitemService: WorkspaceitemDataService, + suggestion: OpenaireSuggestion, + collectionId: string): Observable { + + const resolvedCollectionId = this.resolveCollectionId(suggestion, collectionId); + return workspaceitemService.importExternalSourceEntry(suggestion.externalSourceUri, resolvedCollectionId) + .pipe( + getFirstSucceededRemoteDataPayload(), + catchError((error) => of(null)) + ); + } + + /** + * Perform the delete operation over a single suggestion. + * @param suggestionId + */ + public notMine(suggestionId): Observable> { + return this.deleteReviewedSuggestion(suggestionId).pipe( + catchError((error) => of(null)) + ); + } + + /** + * Perform a bulk approve and import operation. + * @param workspaceitemService injected dependency + * @param suggestions the array containing the suggestions + * @param collectionId the collectionId + */ + public approveAndImportMultiple(workspaceitemService: WorkspaceitemDataService, + suggestions: OpenaireSuggestion[], + collectionId: string): Observable { + + return forkJoin(suggestions.map((suggestion: OpenaireSuggestion) => + this.approveAndImport(workspaceitemService, suggestion, collectionId))) + .pipe(map((results: WorkspaceItem[]) => { + return { + success: results.filter((result) => result != null).length, + fails: results.filter((result) => result == null).length + }; + }), take(1)); + } + + /** + * Perform a bulk notMine operation. + * @param suggestions the array containing the suggestions + */ + public notMineMultiple(suggestions: OpenaireSuggestion[]): Observable { + return forkJoin(suggestions.map((suggestion: OpenaireSuggestion) => this.notMine(suggestion.id))) + .pipe(map((results: RemoteData[]) => { + return { + success: results.filter((result) => result != null).length, + fails: results.filter((result) => result == null).length + }; + }), take(1)); + } + + /** + * Get the researcher uuid (for navigation purpose) from a target instance. + * TODO Find a better way + * @param target + * @return the researchUuid + */ + public getTargetUuid(target: OpenaireSuggestionTarget): string { + const tokens = target.id.split(':'); + return tokens.length === 2 ? tokens[1] : null; + } + + /** + * Interpolated params to build the notification suggestions notification. + * @param suggestionTarget + */ + public getNotificationSuggestionInterpolation(suggestionTarget: OpenaireSuggestionTarget): any { + return { + count: suggestionTarget.total, + source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)), + type: this.translateService.instant(this.translateSuggestionType(suggestionTarget.source)), + suggestionId: suggestionTarget.id, + displayName: suggestionTarget.display + }; + } + + public translateSuggestionType(source: string): string { + return 'reciter.suggestion.type.' + source; + } + + public translateSuggestionSource(source: string): string { + return 'reciter.suggestion.source.' + source; + } + + /** + * If the provided collectionId ha no value, tries to resolve it by suggestion source. + * @param suggestion + * @param collectionId + */ + public resolveCollectionId(suggestion: OpenaireSuggestion, collectionId): string { + if (hasValue(collectionId)) { + return collectionId; + } + return environment.suggestion + .find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source) + .collectionId; + } + + /** + * Return true if all the suggestion are configured with the same fixed collection + * in the configuration. + * @param suggestions + */ + public isCollectionFixed(suggestions: OpenaireSuggestion[]): boolean { + return this.getFixedCollectionIds(suggestions).length === 1; + } + + private getFixedCollectionIds(suggestions: OpenaireSuggestion[]): string[] { + const collectionIds = {}; + suggestions.forEach((suggestion: OpenaireSuggestion) => { + const conf = environment.suggestion.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source); + if (hasValue(conf)) { + collectionIds[conf.collectionId] = true; + } + }); + return Object.keys(collectionIds); + } +} diff --git a/src/app/shared/mocks/openaire.mock.ts b/src/app/shared/mocks/openaire.mock.ts new file mode 100644 index 0000000000..95ea871727 --- /dev/null +++ b/src/app/shared/mocks/openaire.mock.ts @@ -0,0 +1,1797 @@ +import { of as observableOf } from 'rxjs'; +import { ResourceType } from '../../core/shared/resource-type'; +import { OpenaireBrokerTopicObject } from '../../core/openaire/broker/models/openaire-broker-topic.model'; +import { OpenaireBrokerEventObject } from '../../core/openaire/broker/models/openaire-broker-event.model'; +import { OpenaireBrokerTopicRestService } from '../../core/openaire/broker/topics/openaire-broker-topic-rest.service'; +import { OpenaireBrokerEventRestService } from '../../core/openaire/broker/events/openaire-broker-event-rest.service'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { OpenaireStateService } from '../../openaire/openaire-state.service'; +import { Item } from '../../core/shared/item.model'; +import { + createNoContentRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../remote-data.utils'; +import { SearchResult } from '../search/models/search-result.model'; +import { SuggestionsService } from '../../openaire/reciter-suggestions/suggestions.service'; + +// REST Mock --------------------------------------------------------------------- +// ------------------------------------------------------------------------------- + +// Items +// ------------------------------------------------------------------------------- + +const ItemMockPid1: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174001', + uuid: 'ITEM4567-e89b-12d3-a456-426614174001', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Index nominum et rerum' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid2: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174004', + uuid: 'ITEM4567-e89b-12d3-a456-426614174004', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'UNA NUOVA RILETTURA DELL\u0027 ARISTOTELE DI FRANZ BRENTANO ALLA LUCE DI ALCUNI INEDITI' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid3: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174005', + uuid: 'ITEM4567-e89b-12d3-a456-426614174005', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Sustainable development' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid4: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174006', + uuid: 'ITEM4567-e89b-12d3-a456-426614174006', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Reply to Critics' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid5: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174007', + uuid: 'ITEM4567-e89b-12d3-a456-426614174007', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'PROGETTAZIONE, SINTESI E VALUTAZIONE DELL\u0027ATTIVITA\u0027 ANTIMICOBATTERICA ED ANTIFUNGINA DI NUOVI DERIVATI ETEROCICLICI' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid6: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174008', + uuid: 'ITEM4567-e89b-12d3-a456-426614174008', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Donald Davidson' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +const ItemMockPid7: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174009', + uuid: 'ITEM4567-e89b-12d3-a456-426614174009', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Missing abstract article' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +export const ItemMockPid8: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174002', + uuid: 'ITEM4567-e89b-12d3-a456-426614174002', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Egypt, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +export const ItemMockPid9: Item = Object.assign( + new Item(), + { + handle: '10077/21486', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'ITEM4567-e89b-12d3-a456-426614174003', + uuid: 'ITEM4567-e89b-12d3-a456-426614174003', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Morocco, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +export const ItemMockPid10: Item = Object.assign( + new Item(), + { + handle: '10713/29832', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'P23e4567-e89b-12d3-a456-426614174002', + uuid: 'P23e4567-e89b-12d3-a456-426614174002', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +export const OpenaireMockDspaceObject: SearchResult = Object.assign( + new SearchResult(), + { + handle: '10713/29832', + lastModified: '2017-04-24T19:44:08.178+0000', + isArchived: true, + isDiscoverable: true, + isWithdrawn: false, + _links:{ + self: { + href: 'https://rest.api/rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' + } + }, + id: 'P23e4567-e89b-12d3-a456-426614174002', + uuid: 'P23e4567-e89b-12d3-a456-426614174002', + type: 'item', + metadata: { + 'dc.creator': [ + { + language: 'en_US', + value: 'Doe, Jane' + } + ], + 'dc.date.accessioned': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.available': [ + { + language: null, + value: '1650-06-26T19:58:25Z' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '1650-06-26' + } + ], + 'dc.identifier.issn': [ + { + language: 'en_US', + value: '123456789' + } + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'http://dspace7.4science.it/xmlui/handle/10673/6' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!' + } + ], + 'dc.description.provenance': [ + { + language: 'en', + value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).' + }, + { + language: 'en', + value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).' + } + ], + 'dc.language': [ + { + language: 'en_US', + value: 'en' + } + ], + 'dc.rights': [ + { + language: 'en_US', + value: '© Jane Doe' + } + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'keyword1' + }, + { + language: 'en_US', + value: 'keyword2' + }, + { + language: 'en_US', + value: 'keyword3' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' + } + ], + 'dc.type': [ + { + language: 'en_US', + value: 'text' + } + ] + } + } +); + +// Topics +// ------------------------------------------------------------------------------- + +export const openaireBrokerTopicObjectMorePid: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MORE!PID', + name: 'ENRICH/MORE/PID', + lastEvent: '2020/10/09 10:11 UTC', + totalEvents: 33, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MORE!PID' + } + } +}; + +export const openaireBrokerTopicObjectMoreAbstract: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MORE!ABSTRACT', + name: 'ENRICH/MORE/ABSTRACT', + lastEvent: '2020/09/08 21:14 UTC', + totalEvents: 5, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MORE!ABSTRACT' + } + } +}; + +export const openaireBrokerTopicObjectMissingPid: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MISSING!PID', + name: 'ENRICH/MISSING/PID', + lastEvent: '2020/10/01 07:36 UTC', + totalEvents: 4, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!PID' + } + } +}; + +export const openaireBrokerTopicObjectMissingAbstract: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MISSING!ABSTRACT', + name: 'ENRICH/MISSING/ABSTRACT', + lastEvent: '2020/10/08 16:14 UTC', + totalEvents: 71, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!ABSTRACT' + } + } +}; + +export const openaireBrokerTopicObjectMissingAcm: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MISSING!SUBJECT!ACM', + name: 'ENRICH/MISSING/SUBJECT/ACM', + lastEvent: '2020/09/21 17:51 UTC', + totalEvents: 18, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!SUBJECT!ACM' + } + } +}; + +export const openaireBrokerTopicObjectMissingProject: OpenaireBrokerTopicObject = { + type: new ResourceType('nbtopic'), + id: 'ENRICH!MISSING!PROJECT', + name: 'ENRICH/MISSING/PROJECT', + lastEvent: '2020/09/17 10:28 UTC', + totalEvents: 6, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!PROJECT' + } + } +}; + +// Events +// ------------------------------------------------------------------------------- + +export const openaireBrokerEventObjectMissingPid: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174001', + uuid: '123e4567-e89b-12d3-a456-426614174001', + type: new ResourceType('nbevent'), + originalId: 'oai:www.openstarts.units.it:10077/21486', + title: 'Index nominum et rerum', + trust: 0.375, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'doi', + value: '10.18848/1447-9494/cgp/v15i09/45934', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001', + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid1)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingPid2: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174004', + uuid: '123e4567-e89b-12d3-a456-426614174004', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/21486', + title: 'UNA NUOVA RILETTURA DELL\u0027 ARISTOTELE DI FRANZ BRENTANO ALLA LUCE DI ALCUNI INEDITI', + trust: 1.0, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'urn', + value: 'http://thesis2.sba.units.it/store/handle/item/12238', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid2)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingPid3: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174005', + uuid: '123e4567-e89b-12d3-a456-426614174005', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/554', + title: 'Sustainable development', + trust: 0.375, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'doi', + value: '10.4324/9780203408889', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid3)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingPid4: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174006', + uuid: '123e4567-e89b-12d3-a456-426614174006', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/10787', + title: 'Reply to Critics', + trust: 1.0, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'doi', + value: '10.1080/13698230.2018.1430104', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid4)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingPid5: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174007', + uuid: '123e4567-e89b-12d3-a456-426614174007', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/11339', + title: 'PROGETTAZIONE, SINTESI E VALUTAZIONE DELL\u0027ATTIVITA\u0027 ANTIMICOBATTERICA ED ANTIFUNGINA DI NUOVI DERIVATI ETEROCICLICI', + trust: 0.375, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'urn', + value: 'http://thesis2.sba.units.it/store/handle/item/12477', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid5)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingPid6: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174008', + uuid: '123e4567-e89b-12d3-a456-426614174008', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/29860', + title: 'Donald Davidson', + trust: 0.375, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: 'doi', + value: '10.1111/j.1475-4975.2004.00098.x', + abstract: null, + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid6)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingAbstract: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174009', + uuid: '123e4567-e89b-12d3-a456-426614174009', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/21110', + title: 'Missing abstract article', + trust: 0.751, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: null, + value: null, + abstract: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque vestibulum tellus sed lacinia. Aenean vitae sapien a quam congue ultrices. Sed vehicula sollicitudin ligula, vitae lacinia velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque vestibulum tellus sed lacinia. Aenean vitae sapien a quam congue ultrices. Sed vehicula sollicitudin ligula, vitae lacinia velit.', + openaireId: null, + acronym: null, + code: null, + funder: null, + fundingProgram: null, + jurisdiction: null, + title: null + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009/related' + } + }, + target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid7)), + related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) +}; + +export const openaireBrokerEventObjectMissingProjectFound: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174002', + uuid: '123e4567-e89b-12d3-a456-426614174002', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/21838', + title: 'Egypt, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature', + trust: 1.0, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: null, + value: null, + abstract: null, + openaireId: null, + acronym: 'PAThs', + code: '687567', + funder: 'EC', + fundingProgram: 'H2020', + jurisdiction: 'EU', + title: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002/related' + } + }, + target: createSuccessfulRemoteDataObject$(ItemMockPid8), + related: createSuccessfulRemoteDataObject$(ItemMockPid10) +}; + +export const openaireBrokerEventObjectMissingProjectNotFound: OpenaireBrokerEventObject = { + id: '123e4567-e89b-12d3-a456-426614174003', + uuid: '123e4567-e89b-12d3-a456-426614174003', + type: new ResourceType('openaireBrokerEvent'), + originalId: 'oai:www.openstarts.units.it:10077/21838', + title: 'Morocco, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature', + trust: 1.0, + eventDate: '2020/10/09 10:11 UTC', + status: 'PENDING', + message: { + type: null, + value: null, + abstract: null, + openaireId: null, + acronym: 'PAThs', + code: '687567B', + funder: 'EC', + fundingProgram: 'H2021', + jurisdiction: 'EU', + title: 'Tracking Unknown Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' + }, + _links: { + self: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003' + }, + target: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003/target' + }, + related: { + href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003/related' + } + }, + target: createSuccessfulRemoteDataObject$(ItemMockPid9), + related: createNoContentRemoteDataObject$() +}; + +// Classes +// ------------------------------------------------------------------------------- + +/** + * Mock for [[OpenaireStateService]] + */ +export function getMockOpenaireStateService(): any { + return jasmine.createSpyObj('OpenaireStateService', { + getOpenaireBrokerTopics: jasmine.createSpy('getOpenaireBrokerTopics'), + isOpenaireBrokerTopicsLoading: jasmine.createSpy('isOpenaireBrokerTopicsLoading'), + isOpenaireBrokerTopicsLoaded: jasmine.createSpy('isOpenaireBrokerTopicsLoaded'), + isOpenaireBrokerTopicsProcessing: jasmine.createSpy('isOpenaireBrokerTopicsProcessing'), + getOpenaireBrokerTopicsTotalPages: jasmine.createSpy('getOpenaireBrokerTopicsTotalPages'), + getOpenaireBrokerTopicsCurrentPage: jasmine.createSpy('getOpenaireBrokerTopicsCurrentPage'), + getOpenaireBrokerTopicsTotals: jasmine.createSpy('getOpenaireBrokerTopicsTotals'), + dispatchRetrieveOpenaireBrokerTopics: jasmine.createSpy('dispatchRetrieveOpenaireBrokerTopics'), + dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction') + }); +} + +/** + * Mock for [[OpenaireBrokerTopicRestService]] + */ +export function getMockOpenaireBrokerTopicRestService(): OpenaireBrokerTopicRestService { + return jasmine.createSpyObj('OpenaireBrokerTopicRestService', { + getTopics: jasmine.createSpy('getTopics'), + getTopic: jasmine.createSpy('getTopic'), + }); +} + +/** + * Mock for [[OpenaireBrokerEventRestService]] + */ +export function getMockOpenaireBrokerEventRestService(): OpenaireBrokerEventRestService { + return jasmine.createSpyObj('OpenaireBrokerEventRestService', { + getEventsByTopic: jasmine.createSpy('getEventsByTopic'), + getEvent: jasmine.createSpy('getEvent'), + patchEvent: jasmine.createSpy('patchEvent'), + boundProject: jasmine.createSpy('boundProject'), + removeProject: jasmine.createSpy('removeProject'), + clearFindByTopicRequests: jasmine.createSpy('.clearFindByTopicRequests') + }); +} + +/** + * Mock for [[OpenaireBrokerEventRestService]] + */ +export function getMockSuggestionsService(): any { + return jasmine.createSpyObj('SuggestionsService', { + getTargets: jasmine.createSpy('getTargets'), + getSuggestions: jasmine.createSpy('getSuggestions'), + clearSuggestionRequests: jasmine.createSpy('clearSuggestionRequests'), + deleteReviewedSuggestion: jasmine.createSpy('deleteReviewedSuggestion'), + retrieveCurrentUserSuggestions: jasmine.createSpy('retrieveCurrentUserSuggestions'), + getTargetUuid: jasmine.createSpy('getTargetUuid'), + }); +} diff --git a/src/app/shared/mocks/reciter-suggestion-targets.mock.ts b/src/app/shared/mocks/reciter-suggestion-targets.mock.ts new file mode 100644 index 0000000000..42e293e603 --- /dev/null +++ b/src/app/shared/mocks/reciter-suggestion-targets.mock.ts @@ -0,0 +1,42 @@ +import { ResourceType } from '../../core/shared/resource-type'; +import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; + +// REST Mock --------------------------------------------------------------------- +// ------------------------------------------------------------------------------- +export const mockSuggestionTargetsObjectOne: OpenaireSuggestionTarget = { + type: new ResourceType('suggestiontarget'), + id: 'reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26', + display: 'Bollini, Andrea', + source: 'reciter', + total: 31, + _links: { + target: { + href: 'https://rest.api/rest/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26' + }, + suggestions: { + href: 'https://rest.api/rest/api/integration/suggestions/search/findByTargetAndSource?target=gf3d657-9d6d-4a87-b905-fef0f8cae26c&source=reciter' + }, + self: { + href: 'https://rest.api/rest/api/integration/suggestiontargets/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26' + } + } +}; + +export const mockSuggestionTargetsObjectTwo: OpenaireSuggestionTarget = { + type: new ResourceType('suggestiontarget'), + id: 'reciter:nhy567-9d6d-ty67-b905-fef0f8cae26', + display: 'Digilio, Andrea', + source: 'reciter', + total: 12, + _links: { + target: { + href: 'https://rest.api/rest/api/core/items/nhy567-9d6d-ty67-b905-fef0f8cae26' + }, + suggestions: { + href: 'https://rest.api/rest/api/integration/suggestions/search/findByTargetAndSource?target=nhy567-9d6d-ty67-b905-fef0f8cae26&source=reciter' + }, + self: { + href: 'https://rest.api/rest/api/integration/suggestiontargets/reciter:nhy567-9d6d-ty67-b905-fef0f8cae26' + } + } +}; diff --git a/src/app/shared/mocks/reciter-suggestion.mock.ts b/src/app/shared/mocks/reciter-suggestion.mock.ts new file mode 100644 index 0000000000..2ddb49868f --- /dev/null +++ b/src/app/shared/mocks/reciter-suggestion.mock.ts @@ -0,0 +1,210 @@ + +// REST Mock --------------------------------------------------------------------- +// ------------------------------------------------------------------------------- + +import { OpenaireSuggestion } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { SUGGESTION } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-objects.resource-type'; + +export const mockSuggestionPublicationOne: OpenaireSuggestion = { + id: '24694772', + display: 'publication one', + source: 'reciter', + externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772', + score: '48', + evidences: { + acceptedRejectedEvidence: { + score: '2.7', + notes: 'some notes, eventually empty or null' + }, + authorNameEvidence: { + score: '0', + notes: 'some notes, eventually empty or null' + }, + journalCategoryEvidence: { + score: '6', + notes: 'some notes, eventually empty or null' + }, + affiliationEvidence: { + score: 'xxx', + notes: 'some notes, eventually empty or null' + }, + relationshipEvidence: { + score: '9', + notes: 'some notes, eventually empty or null' + }, + educationYearEvidence: { + score: '3.6', + notes: 'some notes, eventually empty or null' + }, + personTypeEvidence: { + score: '4', + notes: 'some notes, eventually empty or null' + }, + articleCountEvidence: { + score: '6.7', + notes: 'some notes, eventually empty or null' + }, + averageClusteringEvidence: { + score: '7', + notes: 'some notes, eventually empty or null' + } + }, + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://publication/0000-0003-3681-2038', + language: null, + authority: null, + confidence: -1, + place: -1 + } as any + ], + 'dc.title': [ + { + value: 'publication one', + language: null, + authority: null, + confidence: -1 + } as any + ], + 'dc.date.issued': [ + { + value: '2010-11-03', + language: null, + authority: null, + confidence: -1 + } as any + ], + 'dspace.entity.type': [ + { + uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06', + language: null, + value: 'OrgUnit', + place: 0, + authority: null, + confidence: -1 + } as any + ], + 'dc.description': [ + { + uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06', + language: null, + value: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).", + place: 0, + authority: null, + confidence: -1 + } as any + ] + }, + type: SUGGESTION, + _links: { + target: { + href: 'https://dspace7.4science.cloud/server/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26' + }, + self: { + href: 'https://dspace7.4science.cloud/server/api/integration/suggestions/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26c:24694772' + } + } +}; + +export const mockSuggestionPublicationTwo: OpenaireSuggestion = { + id: '24694772', + display: 'publication two', + source: 'reciter', + externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772', + score: '48', + evidences: { + acceptedRejectedEvidence: { + score: '2.7', + notes: 'some notes, eventually empty or null' + }, + authorNameEvidence: { + score: '0', + notes: 'some notes, eventually empty or null' + }, + journalCategoryEvidence: { + score: '6', + notes: 'some notes, eventually empty or null' + }, + affiliationEvidence: { + score: 'xxx', + notes: 'some notes, eventually empty or null' + }, + relationshipEvidence: { + score: '9', + notes: 'some notes, eventually empty or null' + }, + educationYearEvidence: { + score: '3.6', + notes: 'some notes, eventually empty or null' + }, + personTypeEvidence: { + score: '4', + notes: 'some notes, eventually empty or null' + }, + articleCountEvidence: { + score: '6.7', + notes: 'some notes, eventually empty or null' + }, + averageClusteringEvidence: { + score: '7', + notes: 'some notes, eventually empty or null' + } + }, + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://publication/0000-0003-3681-2038', + language: null, + authority: null, + confidence: -1, + place: -1 + } as any + ], + 'dc.title': [ + { + value: 'publication one', + language: null, + authority: null, + confidence: -1 + } as any + ], + 'dc.date.issued': [ + { + value: '2010-11-03', + language: null, + authority: null, + confidence: -1 + } as any + ], + 'dspace.entity.type': [ + { + uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06', + language: null, + value: 'OrgUnit', + place: 0, + authority: null, + confidence: -1 + } as any + ], + 'dc.description': [ + { + uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06', + language: null, + value: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).", + place: 0, + authority: null, + confidence: -1 + } as any + ] + }, + type: SUGGESTION, + _links: { + target: { + href: 'https://dspace7.4science.cloud/server/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26' + }, + self: { + href: 'https://dspace7.4science.cloud/server/api/integration/suggestions/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26c:24694772' + } + } +}; diff --git a/src/app/suggestions-page/suggestions-page-routing-paths.ts b/src/app/suggestions-page/suggestions-page-routing-paths.ts new file mode 100644 index 0000000000..0f3aa782d2 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routing-paths.ts @@ -0,0 +1,11 @@ +import { URLCombiner } from '../core/url-combiner/url-combiner'; + +export const SUGGESTION_MODULE_PATH = 'suggestions'; + +export function getSuggestionModuleRoute() { + return `/${SUGGESTION_MODULE_PATH}`; +} + +export function getSuggestionPageRoute(SuggestionId: string) { + return new URLCombiner(getSuggestionModuleRoute(), SuggestionId).toString(); +} diff --git a/src/app/suggestions-page/suggestions-page-routing.module.ts b/src/app/suggestions-page/suggestions-page-routing.module.ts new file mode 100644 index 0000000000..20ed658707 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routing.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { SuggestionsPageResolver } from './suggestions-page.resolver'; +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: ':targetId', + resolve: { + suggestionTargets: SuggestionsPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'admin.notifications.recitersuggestion.page.title', + breadcrumbKey: 'admin.notifications.recitersuggestion', + showBreadcrumbsFluid: false + }, + canActivate: [AuthenticatedGuard], + runGuardsAndResolvers: 'always', + component: SuggestionsPageComponent, + }, + ]) + ], + providers: [ + SuggestionsPageResolver + ] +}) +export class SuggestionsPageRoutingModule { + +} diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html new file mode 100644 index 0000000000..fb5f08b6a4 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -0,0 +1,48 @@ +
+
+
+ +
+ +

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

+ +
+ + ({{ getSelectedSuggestionsCount() }}) + + +
+ + +
    +
  • + +
  • +
+
+
+
+
+
+
diff --git a/src/app/suggestions-page/suggestions-page.component.scss b/src/app/suggestions-page/suggestions-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts new file mode 100644 index 0000000000..ad518930d9 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -0,0 +1,107 @@ +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 { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { SuggestionListElementComponent } from '../openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionsService } from '../openaire/reciter-suggestions/suggestions.service'; +import { getMockOpenaireStateService, getMockSuggestionsService } from '../shared/mocks/openaire.mock'; +import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model'; +import { OpenaireSuggestion } from '../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { mockSuggestionPublicationOne, mockSuggestionPublicationTwo } from '../shared/mocks/reciter-suggestion.mock'; +import { SuggestionEvidencesComponent } from '../openaire/reciter-suggestions/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 { 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 '../openaire/reciter-suggestions/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'; +import { PaginationService } from '../core/pagination/pagination.service'; + +describe('SuggestionPageComponent', () => { + let component: SuggestionsPageComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + const mockSuggestionsService = getMockSuggestionsService(); + const mockSuggestionsTargetStateService = getMockOpenaireStateService(); + const suggestionTargetsList: PaginatedList = buildPaginatedList(new PageInfo(), [mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + const router = new RouterStub(); + const routeStub = { + data: observableOf({ + suggestionTargets: createSuccessfulRemoteDataObject(mockSuggestionTargetsObjectOne) + }), + queryParams: observableOf({}) + }; + const workspaceitemServiceMock = jasmine.createSpyObj('WorkspaceitemDataService', { + importExternalSourceEntry: jasmine.createSpy('importExternalSourceEntry') + }); + + const authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + setRedirectUrl: {} + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + TranslateModule.forRoot() + ], + declarations: [ + SuggestionEvidencesComponent, + SuggestionListElementComponent, + SuggestionsPageComponent, + ObjectKeysPipe, + VarDirective + ], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: WorkspaceitemDataService, useValue: workspaceitemServiceMock }, + { provide: Router, useValue: router }, + { provide: SuggestionsService, useValue: mockSuggestionsService }, + { provide: SuggestionTargetsStateService, useValue: mockSuggestionsTargetStateService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: TranslateService, useValue: getMockTranslateService() }, + { provide: PaginationService, useValue: paginationService }, + SuggestionsPageComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents().then(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionsPageComponent); + component = fixture.componentInstance; + scheduler = getTestScheduler(); + }); + + it('should create', () => { + spyOn(component, 'updatePage').and.stub(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(component).toBeTruthy(); + expect(component.suggestionId).toBe(mockSuggestionTargetsObjectOne.id); + expect(component.researcherName).toBe(mockSuggestionTargetsObjectOne.display); + expect(component.updatePage).toHaveBeenCalled(); + }); +}); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts new file mode 100644 index 0000000000..911f6167de --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -0,0 +1,285 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, Router } from '@angular/router'; + +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { SortDirection, SortOptions, } from '../core/cache/models/sort-options.model'; +import { PaginatedList } from '../core/data/paginated-list.model'; +import { RemoteData } from '../core/data/remote-data'; +import { getFirstSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; +import { SuggestionBulkResult, SuggestionsService } from '../openaire/reciter-suggestions/suggestions.service'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { OpenaireSuggestion } from '../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; +import { OpenaireSuggestionTarget } from '../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { AuthService } from '../core/auth/auth.service'; +import { SuggestionApproveAndImport } from '../openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { SuggestionTargetsStateService } from '../openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { FindListOptions } from '../core/data/request.models'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; + +@Component({ + selector: 'ds-suggestion-page', + templateUrl: './suggestions-page.component.html', + styleUrls: ['./suggestions-page.component.scss'], +}) +export class SuggestionsPageComponent implements OnInit { + + /** + * The pagination configuration + */ + paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'sp', + pageSizeOptions: [5, 10, 20, 40, 60] + }); + + /** + * The sorting configuration + */ + paginationSortConfig: SortOptions = new SortOptions('trust', SortDirection.DESC); + + /** + * The FindListOptions object + */ + defaultConfig: FindListOptions = Object.assign(new FindListOptions(), {sort: this.paginationSortConfig}); + + /** + * A boolean representing if results are loading + */ + public processing$ = new BehaviorSubject(false); + + /** + * A list of remote data objects of suggestions + */ + suggestionsRD$: BehaviorSubject> = new BehaviorSubject>({} as any); + + targetRD$: Observable>; + targetId$: Observable; + + suggestionTarget: OpenaireSuggestionTarget; + suggestionId: any; + suggestionSource: any; + researcherName: any; + researcherUuid: any; + + selectedSuggestions: { [id: string]: OpenaireSuggestion } = {}; + isBulkOperationPending = false; + + constructor( + private authService: AuthService, + private notificationService: NotificationsService, + private paginationService: PaginationService, + private route: ActivatedRoute, + private router: Router, + private suggestionService: SuggestionsService, + private suggestionTargetsStateService: SuggestionTargetsStateService, + private translateService: TranslateService, + private workspaceItemService: WorkspaceitemDataService + ) { + } + + ngOnInit(): void { + this.targetRD$ = this.route.data.pipe( + map((data: Data) => data.suggestionTargets as RemoteData), + redirectOn4xx(this.router, this.authService) + ); + + this.targetId$ = this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((target: OpenaireSuggestionTarget) => target.id) + ); + this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((suggestionTarget: OpenaireSuggestionTarget) => { + this.suggestionTarget = suggestionTarget; + this.suggestionId = suggestionTarget.id; + this.researcherName = suggestionTarget.display; + this.suggestionSource = suggestionTarget.source; + this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); + this.updatePage(); + }); + + this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); + } + + /** + * Called when one of the pagination settings is changed + */ + onPaginationChange() { + this.updatePage(); + } + + /** + * Update the list of suggestions + */ + updatePage() { + this.processing$.next(true); + const pageConfig$: Observable = this.paginationService.getFindListOptions( + this.paginationOptions.id, + this.defaultConfig, + ).pipe( + distinctUntilChanged() + ); + combineLatest([this.targetId$, pageConfig$]).pipe( + switchMap(([targetId, config]: [string, FindListOptions]) => { + return this.suggestionService.getSuggestions( + targetId, + config.elementsPerPage, + config.currentPage, + config.sort + ); + }), + take(1) + ).subscribe((results: PaginatedList) => { + this.processing$.next(false); + this.suggestionsRD$.next(results); + this.suggestionService.clearSuggestionRequests(); + // 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']); + // } + }); + } + + /** + * Used to delete a suggestion. + * @suggestionId + */ + notMine(suggestionId) { + this.suggestionService.notMine(suggestionId).subscribe((res) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + }); + } + + /** + * Used to delete all selected suggestions. + */ + notMineAllSelected() { + this.isBulkOperationPending = true; + this.suggestionService + .notMineMultiple(Object.values(this.selectedSuggestions)) + .subscribe((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('reciter.suggestion.notMine.bulk.success', + {count: results.success})); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('reciter.suggestion.notMine.bulk.error', + {count: results.fails})); + } + }); + } + + /** + * Used to approve & import. + * @param event contains the suggestion and the target collection + */ + approveAndImport(event: SuggestionApproveAndImport) { + this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) + .subscribe((workspaceitem: WorkspaceItem) => { + const content = this.translateService.instant('reciter.suggestion.approveAndImport.success', { workspaceItemId: workspaceitem.id }); + this.notificationService.success('', content, {timeOut:0}, true); + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + }); + } + + /** + * Used to approve & import all selected suggestions. + * @param event contains the target collection + */ + approveAndImportAllSelected(event: SuggestionApproveAndImport) { + this.isBulkOperationPending = true; + this.suggestionService + .approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) + .subscribe((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.updatePage(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('reciter.suggestion.approveAndImport.bulk.success', + {count: results.success})); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('reciter.suggestion.approveAndImport.bulk.error', + {count: results.fails})); + } + }); + } + + /** + * When a specific suggestion is selected. + * @param object the suggestions + * @param selected the new selected value for the suggestion + */ + onSelected(object: OpenaireSuggestion, selected: boolean) { + if (selected) { + this.selectedSuggestions[object.id] = object; + } else { + delete this.selectedSuggestions[object.id]; + } + } + + /** + * When Toggle Select All occurs. + * @param suggestions all the visible suggestions inside the page + */ + onToggleSelectAll(suggestions: OpenaireSuggestion[]) { + if ( this.getSelectedSuggestionsCount() > 0) { + this.selectedSuggestions = {}; + } else { + suggestions.forEach((suggestion) => { + this.selectedSuggestions[suggestion.id] = suggestion; + }); + } + } + + /** + * The current number of selected suggestions. + */ + getSelectedSuggestionsCount(): number { + return Object.keys(this.selectedSuggestions).length; + } + + /** + * Return true if all the suggestion are configured with the same fixed collection in the configuration. + * @param suggestions + */ + isCollectionFixed(suggestions: OpenaireSuggestion[]): boolean { + return this.suggestionService.isCollectionFixed(suggestions); + } + + /** + * Label to be used to translate the suggestion source. + */ + translateSuggestionSource() { + return this.suggestionService.translateSuggestionSource(this.suggestionSource); + } + + /** + * Label to be used to translate the suggestion type. + */ + translateSuggestionType() { + return this.suggestionService.translateSuggestionType(this.suggestionSource); + } + +} diff --git a/src/app/suggestions-page/suggestions-page.module.ts b/src/app/suggestions-page/suggestions-page.module.ts new file mode 100644 index 0000000000..3fad2d0c19 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { SharedModule } from '../shared/shared.module'; +import { SuggestionsPageRoutingModule } from './suggestions-page-routing.module'; +import { SuggestionsService } from '../openaire/reciter-suggestions/suggestions.service'; +import { OpenaireSuggestionsDataService } from '../core/openaire/reciter-suggestions/openaire-suggestions-data.service'; +import { OpenaireModule } from '../openaire/openaire.module'; + +@NgModule({ + declarations: [SuggestionsPageComponent], + imports: [ + CommonModule, + SharedModule, + OpenaireModule, + SuggestionsPageRoutingModule + ], + providers: [ + OpenaireSuggestionsDataService, + SuggestionsService + ] +}) +export class SuggestionsPageModule { } diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts new file mode 100644 index 0000000000..0027ae2e77 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { find } from 'rxjs/operators'; + +import { RemoteData } from '../core/data/remote-data'; +import { hasValue } from '../shared/empty.util'; +import { OpenaireSuggestionsDataService } from '../core/openaire/reciter-suggestions/openaire-suggestions-data.service'; +import { OpenaireSuggestionTarget } from '../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; + +/** + * This class represents a resolver that requests a specific collection before the route is activated + */ +@Injectable() +export class SuggestionsPageResolver implements Resolve> { + constructor(private suggestionsDataService: OpenaireSuggestionsDataService) { + } + + /** + * Method for resolving a suggestion target based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( + find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + ); + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2af9d16b0c..e2919097d6 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -27,6 +27,10 @@ "404.page-not-found": "page not found", + "admin.notifications.recitersuggestion.breadcrumbs": "Suggestions", + + "admin.notifications.recitersuggestion.page.title": "Suggestions", + "admin.curation-tasks.breadcrumbs": "System curation tasks", "admin.curation-tasks.title": "System curation tasks", @@ -2511,6 +2515,7 @@ "menu.section.icon.unpin": "Unpin sidebar", + "menu.section.icon.notifications": "Notifictions menu section", "menu.section.import": "Import", @@ -2533,6 +2538,10 @@ "menu.section.new_process": "Process", + "menu.section.notifications": "Notifications", + + "menu.section.notifications_reciter": "Publication Claim", + "menu.section.pin": "Pin sidebar", @@ -2960,6 +2969,62 @@ "media-viewer.playlist": "Playlist", + "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", + + "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", + "register-email.title": "New user registration", From b4d6fbc390282a381e1f7cc340615c40f39e936a Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 2 May 2022 17:40:51 +0200 Subject: [PATCH 002/159] [CST-5249] Fixed compilation issues --- .../admin-sidebar/admin-sidebar.component.ts | 1 - .../openaire-suggestion-source.model.ts | 2 +- .../openaire-suggestion-target.model.ts | 2 +- .../models/openaire-suggestion.model.ts | 2 +- .../openaire-suggestions-data.service.ts | 4 +- .../submission/workspaceitem-data.service.ts | 31 ++++++++++++- src/app/openaire/openaire.effects.ts | 5 ++ src/app/openaire/openaire.module.ts | 2 + .../openaire/reciter-suggestions/selectors.ts | 6 +-- .../suggestion-list-element.component.html | 1 - .../suggestion-targets.component.html | 1 - .../suggestion-targets.effects.ts | 2 +- .../suggestions.service.ts | 12 ++--- .../suggestions-page.component.ts | 5 +- src/config/app-config.interface.ts | 2 + src/config/default-app-config.ts | 9 ++++ src/config/layout-config.interfaces.ts | 46 +++++++++++++++++++ 17 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 src/app/openaire/openaire.effects.ts create mode 100644 src/config/layout-config.interfaces.ts diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index a8c5ef800c..b770e3d675 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -42,7 +42,6 @@ import { Router, ActivatedRoute } from '@angular/router'; import {NOTIFICATIONS_RECITER_SUGGESTION_PATH} from "../admin-notifications/admin-notifications-routing-paths"; import { MenuID } from '../../shared/menu/menu-id.model'; import { MenuItemType } from '../../shared/menu/menu-item-type.model'; -import { ActivatedRoute } from '@angular/router'; /** * Component representing the admin sidebar diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts index 6da9fd47b9..d71b49b913 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts @@ -1,11 +1,11 @@ import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../../../cache/object-cache.reducer'; import { SUGGESTION_SOURCE } from './openaire-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 {CacheableObject} from "../../../cache/cacheable-object.model"; /** * The interface representing the Suggestion Source model diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts index e35972bc79..06f85ea5b3 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts @@ -1,11 +1,11 @@ import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../../../cache/object-cache.reducer'; import { SUGGESTION_TARGET } from './openaire-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 {CacheableObject} from "../../../cache/cacheable-object.model"; /** * The interface representing the Suggestion Target model diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts index 3ff5d7b630..2d28ccf9bc 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts @@ -1,12 +1,12 @@ import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; -import { CacheableObject } from '../../../cache/object-cache.reducer'; import { SUGGESTION } from './openaire-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"; export interface SuggestionEvidences { [sectionId: string]: { diff --git a/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts index d961eaf9b5..7454caa1d9 100644 --- a/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts +++ b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts @@ -4,14 +4,12 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { CoreState } from '../../core.reducers'; 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 { FindListOptions } from '../../data/request.models'; import { DataService } from '../../data/data.service'; import { ChangeAnalyzer } from '../../data/change-analyzer'; import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; @@ -24,6 +22,8 @@ import { OpenaireSuggestionTarget } from './models/openaire-suggestion-target.mo import { OpenaireSuggestion } from './models/openaire-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"; /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 1b9782834c..a4dbaab6ca 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; import { Store } from '@ngrx/store'; import { dataService } from '../cache/builders/build-decorators'; @@ -17,6 +17,10 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; +import {HttpOptions} from "../dspace-rest/dspace-rest.service"; +import {find, map} from "rxjs/operators"; +import {PostRequest} from "../data/request.models"; +import {hasValue} from "../../shared/empty.util"; /** * A service that provides methods to make REST requests with workspaceitems endpoint. @@ -57,4 +61,29 @@ export class WorkspaceitemDataService extends DataService { return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + /** + * Import an external source entry into a collection + * @param externalSourceEntryHref + * @param collectionId + */ + public importExternalSourceEntry(externalSourceEntryHref: string, collectionId: string): Observable> { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`)); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, externalSourceEntryHref, options); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + } diff --git a/src/app/openaire/openaire.effects.ts b/src/app/openaire/openaire.effects.ts new file mode 100644 index 0000000000..d771089f92 --- /dev/null +++ b/src/app/openaire/openaire.effects.ts @@ -0,0 +1,5 @@ +import { SuggestionTargetsEffects } from './reciter-suggestions/suggestion-targets/suggestion-targets.effects'; + +export const openaireEffects = [ + SuggestionTargetsEffects +]; diff --git a/src/app/openaire/openaire.module.ts b/src/app/openaire/openaire.module.ts index 22d04f3002..ace4622190 100644 --- a/src/app/openaire/openaire.module.ts +++ b/src/app/openaire/openaire.module.ts @@ -18,12 +18,14 @@ import { SuggestionsPopupComponent } from './reciter-suggestions/suggestions-pop import { SuggestionsNotificationComponent } from './reciter-suggestions/suggestions-notification/suggestions-notification.component'; import { TranslateModule } from '@ngx-translate/core'; import { SearchModule } from '../shared/search/search.module'; +import {openaireEffects} from "./openaire.effects"; const MODULES = [ CommonModule, SharedModule, CoreModule.forRoot(), StoreModule.forFeature('openaire', openaireReducers, storeModuleConfig as StoreConfig), + EffectsModule.forFeature(openaireEffects), TranslateModule ]; diff --git a/src/app/openaire/reciter-suggestions/selectors.ts b/src/app/openaire/reciter-suggestions/selectors.ts index e699b27dba..292e63472a 100644 --- a/src/app/openaire/reciter-suggestions/selectors.ts +++ b/src/app/openaire/reciter-suggestions/selectors.ts @@ -1,8 +1,8 @@ -import { createSelector, MemoizedSelector } from '@ngrx/store'; -import { subStateSelector } from '../../shared/selector.util'; +import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/store'; import { openaireSelector, OpenaireState } from '../openaire.reducer'; import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; +import {subStateSelector} from "../../submission/selectors"; /** * Returns the Reciter Suggestion Target state. @@ -10,7 +10,7 @@ import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.r * @param {AppState} state Top level state. * @return {OpenaireState} */ -const _getReciterSuggestionTargetState = (state: any) => state.openaire; +const _getReciterSuggestionTargetState = createFeatureSelector('openaire'); // Reciter Suggestion Targets // ---------------------------------------------------------------------------- diff --git a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html index 05f9c0ac77..f37d595c45 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html +++ b/src/app/openaire/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.html @@ -21,7 +21,6 @@ [showLabel]="false" [object]="listableObject" [linkType]="0" - [hideMetrics]="true" > diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index 85e871403c..9a007fab21 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -81,7 +81,7 @@ export class SuggestionTargetsEffects { return this.store$.select((state: any) => state.core.auth.user) .pipe( switchMap((user: EPerson) => { - return this.suggestionsService.retrieveCurrentUserSuggestions(user) + return this.suggestionsService.retrieveCurrentUserSuggestions(user.uuid) .pipe( map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), catchError((errors) => of(errors)) diff --git a/src/app/openaire/reciter-suggestions/suggestions.service.ts b/src/app/openaire/reciter-suggestions/suggestions.service.ts index de743d95e3..5908c8c6bf 100644 --- a/src/app/openaire/reciter-suggestions/suggestions.service.ts +++ b/src/app/openaire/reciter-suggestions/suggestions.service.ts @@ -5,7 +5,6 @@ import { catchError, map, mergeMap, take } from 'rxjs/operators'; import { OpenaireSuggestionsDataService } from '../../core/openaire/reciter-suggestions/openaire-suggestions-data.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { FindListOptions } from '../../core/data/request.models'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; @@ -25,8 +24,9 @@ import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-da import { TranslateService } from '@ngx-translate/core'; import { NoContent } from '../../core/shared/NoContent.model'; import { environment } from '../../../environments/environment'; -import { SuggestionConfig } from '../../../config/layout-config.interfaces'; import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; +import {FindListOptions} from "../../core/data/find-list-options.model"; +import {SuggestionConfig} from "../../../config/layout-config.interfaces"; export interface SuggestionBulkResult { success: number; @@ -142,11 +142,11 @@ export class SuggestionsService { /** * Retrieve suggestion targets for the given user * - * @param user - * The EPerson object for which to retrieve suggestion targets + * @param userUuid + * The EPerson id for which to retrieve suggestion targets */ - public retrieveCurrentUserSuggestions(user: EPerson): Observable { - return this.researcherProfileService.findById(user.uuid).pipe( + public retrieveCurrentUserSuggestions(userUuid: string): Observable { + return this.researcherProfileService.findById(userUuid).pipe( mergeMap((profile: ResearcherProfile) => { if (isNotEmpty(profile)) { return this.researcherProfileService.findRelatedItemId(profile).pipe( diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index 911f6167de..850a30da22 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -8,7 +8,7 @@ import { TranslateService } from '@ngx-translate/core'; import { SortDirection, SortOptions, } from '../core/cache/models/sort-options.model'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; -import { getFirstSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { SuggestionBulkResult, SuggestionsService } from '../openaire/reciter-suggestions/suggestions.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { OpenaireSuggestion } from '../core/openaire/reciter-suggestions/models/openaire-suggestion.model'; @@ -19,8 +19,9 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { SuggestionTargetsStateService } from '../openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { PaginationService } from '../core/pagination/pagination.service'; -import { FindListOptions } from '../core/data/request.models'; import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import {FindListOptions} from "../core/data/find-list-options.model"; +import {redirectOn4xx} from "../core/shared/authorized.operators"; @Component({ selector: 'ds-suggestion-page', diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 121e80cd74..6446e7f127 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -14,6 +14,7 @@ import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; +import {SuggestionConfig} from "./layout-config.interfaces"; interface AppConfig extends Config { ui: UIServerConfig; @@ -32,6 +33,7 @@ interface AppConfig extends Config { collection: CollectionPageConfig; themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; + suggestion: SuggestionConfig[]; } const APP_CONFIG = new InjectionToken('APP_CONFIG'); diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index dc54c2fcb0..aea29fc819 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -14,6 +14,7 @@ import { ServerConfig } from './server-config.interface'; import { SubmissionConfig } from './submission-config.interface'; import { ThemeConfig } from './theme.model'; import { UIServerConfig } from './ui-server-config.interface'; +import {SuggestionConfig} from "./layout-config.interfaces"; export class DefaultAppConfig implements AppConfig { production = false; @@ -210,6 +211,14 @@ export class DefaultAppConfig implements AppConfig { } }; + suggestion: SuggestionConfig[] = [ + // { + // // Use this configuration to map a suggestion import to a specific collection based on the suggestion type. + // source: 'suggestionSource', + // collectionId: 'collectionUUID' + // } + ]; + // Theme Config themes: ThemeConfig[] = [ // Add additional themes here. In the case where multiple themes match a route, the first one diff --git a/src/config/layout-config.interfaces.ts b/src/config/layout-config.interfaces.ts new file mode 100644 index 0000000000..0b15a06aa9 --- /dev/null +++ b/src/config/layout-config.interfaces.ts @@ -0,0 +1,46 @@ +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; +} From 11d25e66997d99bd1cb932c057feff3f8dc30e70 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 4 May 2022 13:16:13 +0200 Subject: [PATCH 003/159] [CST-5249] Fixed suggestion page --- .../admin-sidebar/admin-sidebar.component.ts | 2 +- src/app/core/core.module.ts | 2 +- .../openaire-suggestion-source.model.ts | 2 +- .../openaire-suggestion-target.model.ts | 2 +- .../models/openaire-suggestion.model.ts | 2 +- .../openaire-suggestions-data.service.ts | 5 +- .../submission/workspaceitem-data.service.ts | 8 +- src/app/openaire/openaire.module.ts | 2 +- .../openaire/reciter-suggestions/selectors.ts | 2 +- .../suggestion-targets.actions.ts | 1 + .../suggestion-targets.effects.ts | 6 +- .../suggestions.service.ts | 4 +- .../dso-selector-modal-wrapper.component.ts | 24 +- src/app/shared/mocks/openaire.mock.ts | 445 ------------------ .../suggestions-page.component.ts | 4 +- src/assets/i18n/en.json5 | 1 + src/config/app-config.interface.ts | 2 +- src/config/default-app-config.ts | 2 +- src/environments/environment.test.ts | 8 + 19 files changed, 55 insertions(+), 469 deletions(-) diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index b770e3d675..c13599be9d 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -39,7 +39,7 @@ import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { Router, ActivatedRoute } from '@angular/router'; -import {NOTIFICATIONS_RECITER_SUGGESTION_PATH} from "../admin-notifications/admin-notifications-routing-paths"; +import {NOTIFICATIONS_RECITER_SUGGESTION_PATH} from '../admin-notifications/admin-notifications-routing-paths'; import { MenuID } from '../../shared/menu/menu-id.model'; import { MenuItemType } from '../../shared/menu/menu-item-type.model'; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 7273996401..879ba76c7d 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -168,7 +168,7 @@ import { OpenaireSuggestionSource } from './openaire/reciter-suggestions/models/ import { ResearcherProfileService } from './profile/researcher-profile.service'; import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; import { ResearcherProfile } from './profile/model/researcher-profile.model'; -import {SubmissionAccessesModel} from "./config/models/config-submission-accesses.model"; +import {SubmissionAccessesModel} from './config/models/config-submission-accesses.model'; /** * When not in production, endpoint responses can be mocked for testing purposes diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts index d71b49b913..00f5f11936 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-source.model.ts @@ -5,7 +5,7 @@ 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 {CacheableObject} from '../../../cache/cacheable-object.model'; /** * The interface representing the Suggestion Source model diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts index 06f85ea5b3..96a43c9654 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion-target.model.ts @@ -5,7 +5,7 @@ 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 {CacheableObject} from '../../../cache/cacheable-object.model'; /** * The interface representing the Suggestion Target model diff --git a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts index 2d28ccf9bc..0f84072c6b 100644 --- a/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts +++ b/src/app/core/openaire/reciter-suggestions/models/openaire-suggestion.model.ts @@ -6,7 +6,7 @@ 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 {CacheableObject} from '../../../cache/cacheable-object.model'; export interface SuggestionEvidences { [sectionId: string]: { diff --git a/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts index 7454caa1d9..3c1d04c040 100644 --- a/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts +++ b/src/app/core/openaire/reciter-suggestions/openaire-suggestions-data.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; @@ -22,8 +23,8 @@ import { OpenaireSuggestionTarget } from './models/openaire-suggestion-target.mo import { OpenaireSuggestion } from './models/openaire-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 {CoreState} from '../../core-state.model'; +import {FindListOptions} from '../../data/find-list-options.model'; /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index a4dbaab6ca..6c0e909bb4 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -17,10 +17,10 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; -import {HttpOptions} from "../dspace-rest/dspace-rest.service"; -import {find, map} from "rxjs/operators"; -import {PostRequest} from "../data/request.models"; -import {hasValue} from "../../shared/empty.util"; +import {HttpOptions} from '../dspace-rest/dspace-rest.service'; +import {find, map} from 'rxjs/operators'; +import {PostRequest} from '../data/request.models'; +import {hasValue} from '../../shared/empty.util'; /** * A service that provides methods to make REST requests with workspaceitems endpoint. diff --git a/src/app/openaire/openaire.module.ts b/src/app/openaire/openaire.module.ts index ace4622190..8ae52b27f4 100644 --- a/src/app/openaire/openaire.module.ts +++ b/src/app/openaire/openaire.module.ts @@ -18,7 +18,7 @@ import { SuggestionsPopupComponent } from './reciter-suggestions/suggestions-pop import { SuggestionsNotificationComponent } from './reciter-suggestions/suggestions-notification/suggestions-notification.component'; import { TranslateModule } from '@ngx-translate/core'; import { SearchModule } from '../shared/search/search.module'; -import {openaireEffects} from "./openaire.effects"; +import {openaireEffects} from './openaire.effects'; const MODULES = [ CommonModule, diff --git a/src/app/openaire/reciter-suggestions/selectors.ts b/src/app/openaire/reciter-suggestions/selectors.ts index 292e63472a..cfbb36d4c2 100644 --- a/src/app/openaire/reciter-suggestions/selectors.ts +++ b/src/app/openaire/reciter-suggestions/selectors.ts @@ -2,7 +2,7 @@ import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/sto import { openaireSelector, OpenaireState } from '../openaire.reducer'; import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; -import {subStateSelector} from "../../submission/selectors"; +import {subStateSelector} from '../../submission/selectors'; /** * Returns the Reciter Suggestion Target state. diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts index 6c44d40b91..33dfb1474b 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; import { type } from '../../../shared/ngrx/type'; import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index 9a007fab21..29672ad49b 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -78,10 +78,10 @@ export class SuggestionTargetsEffects { @Effect() refreshUserTargets$ = this.actions$.pipe( ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), switchMap((action: RefreshUserSuggestionsAction) => { - return this.store$.select((state: any) => state.core.auth.user) + return this.store$.select((state: any) => state.core.auth.userId) .pipe( - switchMap((user: EPerson) => { - return this.suggestionsService.retrieveCurrentUserSuggestions(user.uuid) + switchMap((userId: string) => { + return this.suggestionsService.retrieveCurrentUserSuggestions(userId) .pipe( map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), catchError((errors) => of(errors)) diff --git a/src/app/openaire/reciter-suggestions/suggestions.service.ts b/src/app/openaire/reciter-suggestions/suggestions.service.ts index 5908c8c6bf..67b496b903 100644 --- a/src/app/openaire/reciter-suggestions/suggestions.service.ts +++ b/src/app/openaire/reciter-suggestions/suggestions.service.ts @@ -25,8 +25,8 @@ 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/layout-config.interfaces"; +import {FindListOptions} from '../../core/data/find-list-options.model'; +import {SuggestionConfig} from '../../../config/layout-config.interfaces'; export interface SuggestionBulkResult { success: number; diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index ca8343cfad..3b3ef59ae5 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -21,11 +21,22 @@ export enum SelectorActionType { template: '' }) export abstract class DSOSelectorModalWrapperComponent implements OnInit { + + /** + * The discovery configuration. + */ + @Input() configuration = 'default'; + /** * The current page's DSO */ @Input() dsoRD: RemoteData; + /** + * Representing if component should emit value of selected entries or navigate + */ + @Input() emitOnly = false; + /** * Optional header to display above the selection list * Supports i18n keys @@ -47,6 +58,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ action: SelectorActionType; + /** + * Event emitted when a DSO entry is selected if emitOnly is set to true + */ + @Output() select: EventEmitter = new EventEmitter(); + constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) { } @@ -85,7 +101,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ selectObject(dso: DSpaceObject) { this.close(); - this.navigate(dso); + if (this.emitOnly) { + this.select.emit(dso); + } else { + this.navigate(dso); + } } /** diff --git a/src/app/shared/mocks/openaire.mock.ts b/src/app/shared/mocks/openaire.mock.ts index 95ea871727..d6c50510cd 100644 --- a/src/app/shared/mocks/openaire.mock.ts +++ b/src/app/shared/mocks/openaire.mock.ts @@ -1,17 +1,5 @@ -import { of as observableOf } from 'rxjs'; -import { ResourceType } from '../../core/shared/resource-type'; -import { OpenaireBrokerTopicObject } from '../../core/openaire/broker/models/openaire-broker-topic.model'; -import { OpenaireBrokerEventObject } from '../../core/openaire/broker/models/openaire-broker-event.model'; -import { OpenaireBrokerTopicRestService } from '../../core/openaire/broker/topics/openaire-broker-topic-rest.service'; -import { OpenaireBrokerEventRestService } from '../../core/openaire/broker/events/openaire-broker-event-rest.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { OpenaireStateService } from '../../openaire/openaire-state.service'; import { Item } from '../../core/shared/item.model'; -import { - createNoContentRemoteDataObject$, - createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$ -} from '../remote-data.utils'; import { SearchResult } from '../search/models/search-result.model'; import { SuggestionsService } from '../../openaire/reciter-suggestions/suggestions.service'; @@ -1330,414 +1318,6 @@ export const OpenaireMockDspaceObject: SearchResult = Object.assig } ); -// Topics -// ------------------------------------------------------------------------------- - -export const openaireBrokerTopicObjectMorePid: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MORE!PID', - name: 'ENRICH/MORE/PID', - lastEvent: '2020/10/09 10:11 UTC', - totalEvents: 33, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MORE!PID' - } - } -}; - -export const openaireBrokerTopicObjectMoreAbstract: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MORE!ABSTRACT', - name: 'ENRICH/MORE/ABSTRACT', - lastEvent: '2020/09/08 21:14 UTC', - totalEvents: 5, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MORE!ABSTRACT' - } - } -}; - -export const openaireBrokerTopicObjectMissingPid: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MISSING!PID', - name: 'ENRICH/MISSING/PID', - lastEvent: '2020/10/01 07:36 UTC', - totalEvents: 4, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!PID' - } - } -}; - -export const openaireBrokerTopicObjectMissingAbstract: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MISSING!ABSTRACT', - name: 'ENRICH/MISSING/ABSTRACT', - lastEvent: '2020/10/08 16:14 UTC', - totalEvents: 71, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!ABSTRACT' - } - } -}; - -export const openaireBrokerTopicObjectMissingAcm: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MISSING!SUBJECT!ACM', - name: 'ENRICH/MISSING/SUBJECT/ACM', - lastEvent: '2020/09/21 17:51 UTC', - totalEvents: 18, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!SUBJECT!ACM' - } - } -}; - -export const openaireBrokerTopicObjectMissingProject: OpenaireBrokerTopicObject = { - type: new ResourceType('nbtopic'), - id: 'ENRICH!MISSING!PROJECT', - name: 'ENRICH/MISSING/PROJECT', - lastEvent: '2020/09/17 10:28 UTC', - totalEvents: 6, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbtopics/ENRICH!MISSING!PROJECT' - } - } -}; - -// Events -// ------------------------------------------------------------------------------- - -export const openaireBrokerEventObjectMissingPid: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174001', - uuid: '123e4567-e89b-12d3-a456-426614174001', - type: new ResourceType('nbevent'), - originalId: 'oai:www.openstarts.units.it:10077/21486', - title: 'Index nominum et rerum', - trust: 0.375, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'doi', - value: '10.18848/1447-9494/cgp/v15i09/45934', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001', - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174001/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid1)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingPid2: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174004', - uuid: '123e4567-e89b-12d3-a456-426614174004', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/21486', - title: 'UNA NUOVA RILETTURA DELL\u0027 ARISTOTELE DI FRANZ BRENTANO ALLA LUCE DI ALCUNI INEDITI', - trust: 1.0, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'urn', - value: 'http://thesis2.sba.units.it/store/handle/item/12238', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174004/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid2)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingPid3: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174005', - uuid: '123e4567-e89b-12d3-a456-426614174005', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/554', - title: 'Sustainable development', - trust: 0.375, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'doi', - value: '10.4324/9780203408889', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174005/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid3)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingPid4: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174006', - uuid: '123e4567-e89b-12d3-a456-426614174006', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/10787', - title: 'Reply to Critics', - trust: 1.0, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'doi', - value: '10.1080/13698230.2018.1430104', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174006/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid4)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingPid5: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174007', - uuid: '123e4567-e89b-12d3-a456-426614174007', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/11339', - title: 'PROGETTAZIONE, SINTESI E VALUTAZIONE DELL\u0027ATTIVITA\u0027 ANTIMICOBATTERICA ED ANTIFUNGINA DI NUOVI DERIVATI ETEROCICLICI', - trust: 0.375, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'urn', - value: 'http://thesis2.sba.units.it/store/handle/item/12477', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174007/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid5)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingPid6: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174008', - uuid: '123e4567-e89b-12d3-a456-426614174008', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/29860', - title: 'Donald Davidson', - trust: 0.375, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: 'doi', - value: '10.1111/j.1475-4975.2004.00098.x', - abstract: null, - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174008/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid6)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingAbstract: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174009', - uuid: '123e4567-e89b-12d3-a456-426614174009', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/21110', - title: 'Missing abstract article', - trust: 0.751, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: null, - value: null, - abstract: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque vestibulum tellus sed lacinia. Aenean vitae sapien a quam congue ultrices. Sed vehicula sollicitudin ligula, vitae lacinia velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla scelerisque vestibulum tellus sed lacinia. Aenean vitae sapien a quam congue ultrices. Sed vehicula sollicitudin ligula, vitae lacinia velit.', - openaireId: null, - acronym: null, - code: null, - funder: null, - fundingProgram: null, - jurisdiction: null, - title: null - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174009/related' - } - }, - target: observableOf(createSuccessfulRemoteDataObject(ItemMockPid7)), - related: observableOf(createSuccessfulRemoteDataObject(ItemMockPid10)) -}; - -export const openaireBrokerEventObjectMissingProjectFound: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174002', - uuid: '123e4567-e89b-12d3-a456-426614174002', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/21838', - title: 'Egypt, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature', - trust: 1.0, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: null, - value: null, - abstract: null, - openaireId: null, - acronym: 'PAThs', - code: '687567', - funder: 'EC', - fundingProgram: 'H2020', - jurisdiction: 'EU', - title: 'Tracking Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174002/related' - } - }, - target: createSuccessfulRemoteDataObject$(ItemMockPid8), - related: createSuccessfulRemoteDataObject$(ItemMockPid10) -}; - -export const openaireBrokerEventObjectMissingProjectNotFound: OpenaireBrokerEventObject = { - id: '123e4567-e89b-12d3-a456-426614174003', - uuid: '123e4567-e89b-12d3-a456-426614174003', - type: new ResourceType('openaireBrokerEvent'), - originalId: 'oai:www.openstarts.units.it:10077/21838', - title: 'Morocco, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature', - trust: 1.0, - eventDate: '2020/10/09 10:11 UTC', - status: 'PENDING', - message: { - type: null, - value: null, - abstract: null, - openaireId: null, - acronym: 'PAThs', - code: '687567B', - funder: 'EC', - fundingProgram: 'H2021', - jurisdiction: 'EU', - title: 'Tracking Unknown Papyrus and Parchment Paths: An Archaeological Atlas of Coptic Literature.\nLiterary Texts in their Geographical Context: Production, Copying, Usage, Dissemination and Storage' - }, - _links: { - self: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003' - }, - target: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003/target' - }, - related: { - href: 'https://rest.api/rest/api/integration/nbevents/123e4567-e89b-12d3-a456-426614174003/related' - } - }, - target: createSuccessfulRemoteDataObject$(ItemMockPid9), - related: createNoContentRemoteDataObject$() -}; - // Classes // ------------------------------------------------------------------------------- @@ -1757,31 +1337,6 @@ export function getMockOpenaireStateService(): any { dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction') }); } - -/** - * Mock for [[OpenaireBrokerTopicRestService]] - */ -export function getMockOpenaireBrokerTopicRestService(): OpenaireBrokerTopicRestService { - return jasmine.createSpyObj('OpenaireBrokerTopicRestService', { - getTopics: jasmine.createSpy('getTopics'), - getTopic: jasmine.createSpy('getTopic'), - }); -} - -/** - * Mock for [[OpenaireBrokerEventRestService]] - */ -export function getMockOpenaireBrokerEventRestService(): OpenaireBrokerEventRestService { - return jasmine.createSpyObj('OpenaireBrokerEventRestService', { - getEventsByTopic: jasmine.createSpy('getEventsByTopic'), - getEvent: jasmine.createSpy('getEvent'), - patchEvent: jasmine.createSpy('patchEvent'), - boundProject: jasmine.createSpy('boundProject'), - removeProject: jasmine.createSpy('removeProject'), - clearFindByTopicRequests: jasmine.createSpy('.clearFindByTopicRequests') - }); -} - /** * Mock for [[OpenaireBrokerEventRestService]] */ diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index 850a30da22..3d6ced5ef5 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -20,8 +20,8 @@ import { SuggestionTargetsStateService } from '../openaire/reciter-suggestions/s 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 {FindListOptions} from '../core/data/find-list-options.model'; +import {redirectOn4xx} from '../core/shared/authorized.operators'; @Component({ selector: 'ds-suggestion-page', diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fbc04033d6..f79cf799e5 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3033,6 +3033,7 @@ "reciter.suggestion.totalScore": "Total Score", + "reciter.suggestion.type.oaire": "OpenAIRE", "register-email.title": "New user registration", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 6446e7f127..fbd84fae84 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 './layout-config.interfaces'; interface AppConfig extends Config { ui: UIServerConfig; diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index aea29fc819..9def4849f5 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.model'; import { UIServerConfig } from './ui-server-config.interface'; -import {SuggestionConfig} from "./layout-config.interfaces"; +import {SuggestionConfig} from './layout-config.interfaces'; export class DefaultAppConfig implements AppConfig { production = false; diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 7c24ef8f05..6bf3a9ab8f 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -2,6 +2,7 @@ import { BuildConfig } from 'src/config/build-config.interface'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; +import {SuggestionConfig} from '../config/layout-config.interfaces'; export const environment: BuildConfig = { production: false, @@ -203,6 +204,13 @@ export const environment: BuildConfig = { undoTimeout: 10000 // 10 seconds } }, + suggestion: [ + // { + // // Use this configuration to map a suggestion import to a specific collection based on the suggestion type. + // source: 'suggestionSource', + // collectionId: 'collectionUUID' + // } + ], themes: [ { name: 'full-item-page-theme', From 63bba677bbcffda2554b263ef38f06c5e31aae00 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 7 Jul 2022 17:09:48 +0200 Subject: [PATCH 004/159] [CST-5249] Removed Openaire prefix on Suggestion classes --- .../admin-notifications-routing.module.ts | 1 + src/app/core/core.module.ts | 12 ++-- ...ts => suggestion-objects.resource-type.ts} | 0 ...ce.model.ts => suggestion-source.model.ts} | 4 +- ...et.model.ts => suggestion-target.model.ts} | 4 +- ...uggestion.model.ts => suggestion.model.ts} | 4 +- ...service.ts => suggestions-data.service.ts} | 68 +++++++++---------- .../mocks/reciter-suggestion-targets.mock.ts | 6 +- .../shared/mocks/reciter-suggestion.mock.ts | 8 +-- .../{openaire.mock.ts => suggestion.mock.ts} | 0 .../reciter-suggestions/selectors.ts | 10 +-- .../suggestion-actions.component.ts | 4 +- .../suggestion-evidences.component.ts | 2 +- .../suggestion-list-element.component.ts | 6 +- .../suggestion-targets.actions.ts | 10 +-- .../suggestion-targets.component.ts | 6 +- .../suggestion-targets.effects.ts | 8 +-- .../suggestion-targets.reducer.ts | 6 +- .../suggestion-targets.state.service.ts | 6 +- .../suggestions-notification.component.ts | 6 +- .../suggestions-popup.component.ts | 6 +- .../suggestions.service.ts | 42 ++++++------ src/app/suggestion-notifications/selectors.ts | 2 +- .../suggestion-notifications.module.ts | 6 +- .../suggestion-notifications.reducer.ts | 2 +- .../suggestions-page.component.spec.ts | 6 +- .../suggestions-page.component.ts | 26 +++---- .../suggestions-page.module.ts | 4 +- .../suggestions-page.resolver.ts | 10 +-- 29 files changed, 138 insertions(+), 137 deletions(-) rename src/app/core/suggestion-notifications/reciter-suggestions/models/{openaire-suggestion-objects.resource-type.ts => suggestion-objects.resource-type.ts} (100%) rename src/app/core/suggestion-notifications/reciter-suggestions/models/{openaire-suggestion-source.model.ts => suggestion-source.model.ts} (87%) rename src/app/core/suggestion-notifications/reciter-suggestions/models/{openaire-suggestion-target.model.ts => suggestion-target.model.ts} (89%) rename src/app/core/suggestion-notifications/reciter-suggestions/models/{openaire-suggestion.model.ts => suggestion.model.ts} (92%) rename src/app/core/suggestion-notifications/reciter-suggestions/{openaire-suggestions-data.service.ts => suggestions-data.service.ts} (79%) rename src/app/shared/mocks/{openaire.mock.ts => suggestion.mock.ts} (100%) diff --git a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts index ca6a8cf572..60fa679777 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts @@ -87,6 +87,7 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon I18nBreadcrumbsService, AdminNotificationsSuggestionTargetsPageResolver, SourceDataResolver, + AdminQualityAssuranceSourcePageResolver, AdminQualityAssuranceTopicsPageResolver, AdminQualityAssuranceEventsPageResolver, ] diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index bd82e0adc0..8986fa76ff 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -166,9 +166,9 @@ import { SearchConfig } from './shared/search/search-filters/search-config.model import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; -import { OpenaireSuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; -import { OpenaireSuggestion } from './suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; -import { OpenaireSuggestionSource } from './suggestion-notifications/reciter-suggestions/models/openaire-suggestion-source.model'; +import { SuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; +import { Suggestion } from './suggestion-notifications/reciter-suggestions/models/suggestion.model'; +import { SuggestionSource } from './suggestion-notifications/reciter-suggestions/models/suggestion-source.model'; import { ResearcherProfileService } from './profile/researcher-profile.service'; import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; import { ResearcherProfile } from './profile/model/researcher-profile.model'; @@ -372,9 +372,9 @@ export const models = ShortLivedToken, Registration, UsageReport, - OpenaireSuggestion, - OpenaireSuggestionTarget, - OpenaireSuggestionSource, + Suggestion, + SuggestionTarget, + SuggestionSource, QualityAssuranceTopicObject, QualityAssuranceEventObject, Root, diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts similarity index 100% rename from src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-objects.resource-type.ts rename to src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-source.model.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts similarity index 87% rename from src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-source.model.ts rename to src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts index 00f5f11936..64ddf9863c 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-source.model.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts @@ -1,6 +1,6 @@ import { autoserialize, deserialize } from 'cerialize'; -import { SUGGESTION_SOURCE } from './openaire-suggestion-objects.resource-type'; +import { SUGGESTION_SOURCE } from './suggestion-objects.resource-type'; import { excludeFromEquals } from '../../../utilities/equals.decorators'; import { ResourceType } from '../../../shared/resource-type'; import { HALLink } from '../../../shared/hal-link.model'; @@ -11,7 +11,7 @@ import {CacheableObject} from '../../../cache/cacheable-object.model'; * The interface representing the Suggestion Source model */ @typedObject -export class OpenaireSuggestionSource implements CacheableObject { +export class SuggestionSource implements CacheableObject { /** * A string representing the kind of object, e.g. community, item, … */ diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts similarity index 89% rename from src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model.ts rename to src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts index 96a43c9654..fa23dd0ffa 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts @@ -1,6 +1,6 @@ import { autoserialize, deserialize } from 'cerialize'; -import { SUGGESTION_TARGET } from './openaire-suggestion-objects.resource-type'; +import { SUGGESTION_TARGET } from './suggestion-objects.resource-type'; import { excludeFromEquals } from '../../../utilities/equals.decorators'; import { ResourceType } from '../../../shared/resource-type'; import { HALLink } from '../../../shared/hal-link.model'; @@ -11,7 +11,7 @@ import {CacheableObject} from '../../../cache/cacheable-object.model'; * The interface representing the Suggestion Target model */ @typedObject -export class OpenaireSuggestionTarget implements CacheableObject { +export class SuggestionTarget implements CacheableObject { /** * A string representing the kind of object, e.g. community, item, … */ diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion.model.ts similarity index 92% rename from src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model.ts rename to src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion.model.ts index 0f84072c6b..c36d36794b 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion.model.ts @@ -1,6 +1,6 @@ import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; -import { SUGGESTION } from './openaire-suggestion-objects.resource-type'; +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'; @@ -18,7 +18,7 @@ export interface SuggestionEvidences { * The interface representing the Suggestion Source model */ @typedObject -export class OpenaireSuggestion implements CacheableObject { +export class Suggestion implements CacheableObject { /** * A string representing the kind of object, e.g. community, item, … */ diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service.ts b/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts similarity index 79% rename from src/app/core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service.ts rename to src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts index 3c1d04c040..880c7b2f33 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts @@ -15,12 +15,12 @@ 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 { SUGGESTION_TARGET } from './models/openaire-suggestion-objects.resource-type'; +import { SUGGESTION_TARGET } from './models/suggestion-objects.resource-type'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list.model'; -import { OpenaireSuggestionSource } from './models/openaire-suggestion-source.model'; -import { OpenaireSuggestionTarget } from './models/openaire-suggestion-target.model'; -import { OpenaireSuggestion } from './models/openaire-suggestion.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'; @@ -31,7 +31,7 @@ import {FindListOptions} from '../../data/find-list-options.model'; /** * A private DataService implementation to delegate specific methods to. */ -class SuggestionDataServiceImpl extends DataService { +class SuggestionDataServiceImpl extends DataService { /** * The REST endpoint. */ @@ -46,7 +46,7 @@ class SuggestionDataServiceImpl extends DataService { * @param {HALEndpointService} halService * @param {NotificationsService} notificationsService * @param {HttpClient} http - * @param {ChangeAnalyzer} comparator + * @param {ChangeAnalyzer} comparator */ constructor( protected requestService: RequestService, @@ -56,7 +56,7 @@ class SuggestionDataServiceImpl extends DataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: ChangeAnalyzer) { + protected comparator: ChangeAnalyzer) { super(); } } @@ -64,7 +64,7 @@ class SuggestionDataServiceImpl extends DataService { /** * A private DataService implementation to delegate specific methods to. */ -class SuggestionTargetsDataServiceImpl extends DataService { +class SuggestionTargetsDataServiceImpl extends DataService { /** * The REST endpoint. */ @@ -79,7 +79,7 @@ class SuggestionTargetsDataServiceImpl extends DataService} comparator + * @param {ChangeAnalyzer} comparator */ constructor( protected requestService: RequestService, @@ -89,7 +89,7 @@ class SuggestionTargetsDataServiceImpl extends DataService) { + protected comparator: ChangeAnalyzer) { super(); } } @@ -97,7 +97,7 @@ class SuggestionTargetsDataServiceImpl extends DataService { +class SuggestionSourcesDataServiceImpl extends DataService { /** * The REST endpoint. */ @@ -112,7 +112,7 @@ class SuggestionSourcesDataServiceImpl extends DataService} comparator + * @param {ChangeAnalyzer} comparator */ constructor( protected requestService: RequestService, @@ -122,7 +122,7 @@ class SuggestionSourcesDataServiceImpl extends DataService) { + protected comparator: ChangeAnalyzer) { super(); } } @@ -132,7 +132,7 @@ class SuggestionSourcesDataServiceImpl extends DataService} comparatorSuggestions - * @param {DefaultChangeAnalyzer} comparatorSources - * @param {DefaultChangeAnalyzer} comparatorTargets + * @param {DefaultChangeAnalyzer} comparatorSuggestions + * @param {DefaultChangeAnalyzer} comparatorSources + * @param {DefaultChangeAnalyzer} comparatorTargets */ constructor( protected requestService: RequestService, @@ -171,9 +171,9 @@ export class OpenaireSuggestionsDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparatorSuggestions: DefaultChangeAnalyzer, - protected comparatorSources: DefaultChangeAnalyzer, - protected comparatorTargets: DefaultChangeAnalyzer, + protected comparatorSuggestions: DefaultChangeAnalyzer, + protected comparatorSources: DefaultChangeAnalyzer, + protected comparatorTargets: DefaultChangeAnalyzer, ) { this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions); this.suggestionSourcesDataService = new SuggestionSourcesDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources); @@ -185,10 +185,10 @@ export class OpenaireSuggestionsDataService { * * @param options * Find list options object. - * @return Observable>> + * @return Observable>> * The list of Suggestion Sources. */ - public getSources(options: FindListOptions = {}): Observable>> { + public getSources(options: FindListOptions = {}): Observable>> { return this.suggestionSourcesDataService.findAll(options); } @@ -201,14 +201,14 @@ export class OpenaireSuggestionsDataService { * Find list options object. * @param linksToFollow * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. - * @return Observable>> + * @return Observable>> * The list of Suggestion Target. */ public getTargets( source: string, options: FindListOptions = {}, - ...linksToFollow: FollowLinkConfig[] - ): Observable>> { + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { options.searchParams = [new RequestParam('source', source)]; return this.suggestionTargetsDataService.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow); @@ -223,14 +223,14 @@ export class OpenaireSuggestionsDataService { * Find list options object. * @param linksToFollow * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. - * @return Observable>> + * @return Observable>> * The list of Suggestion Target. */ public getTargetsByUser( userId: string, options: FindListOptions = {}, - ...linksToFollow: FollowLinkConfig[] - ): Observable>> { + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { options.searchParams = [new RequestParam('target', userId)]; return this.suggestionTargetsDataService.searchBy(this.searchFindByTargetMethod, options, true, true, ...linksToFollow); @@ -242,10 +242,10 @@ export class OpenaireSuggestionsDataService { * @param targetId * The target id to retrieve. * - * @return Observable> + * @return Observable> * The list of Suggestion Target. */ - public getTargetById(targetId: string): Observable> { + public getTargetById(targetId: string): Observable> { return this.suggestionTargetsDataService.findById(targetId); } @@ -261,7 +261,7 @@ export class OpenaireSuggestionsDataService { * Used to fetch Suggestion notification for user * @suggestionId */ - public getSuggestion(suggestionId: string, ...linksToFollow: FollowLinkConfig[]): Observable> { + public getSuggestion(suggestionId: string, ...linksToFollow: FollowLinkConfig[]): Observable> { return this.suggestionsDataService.findById(suggestionId, true, true, ...linksToFollow); } @@ -276,15 +276,15 @@ export class OpenaireSuggestionsDataService { * Find list options object. * @param linksToFollow * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. - * @return Observable>> + * @return Observable>> * The list of Suggestion. */ public getSuggestionsByTargetAndSource( target: string, source: string, options: FindListOptions = {}, - ...linksToFollow: FollowLinkConfig[] - ): Observable>> { + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { options.searchParams = [ new RequestParam('target', target), new RequestParam('source', source) diff --git a/src/app/shared/mocks/reciter-suggestion-targets.mock.ts b/src/app/shared/mocks/reciter-suggestion-targets.mock.ts index ae8cacb273..489bb7733d 100644 --- a/src/app/shared/mocks/reciter-suggestion-targets.mock.ts +++ b/src/app/shared/mocks/reciter-suggestion-targets.mock.ts @@ -1,9 +1,9 @@ import { ResourceType } from '../../core/shared/resource-type'; -import { OpenaireSuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- -export const mockSuggestionTargetsObjectOne: OpenaireSuggestionTarget = { +export const mockSuggestionTargetsObjectOne: SuggestionTarget = { type: new ResourceType('suggestiontarget'), id: 'reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26', display: 'Bollini, Andrea', @@ -22,7 +22,7 @@ export const mockSuggestionTargetsObjectOne: OpenaireSuggestionTarget = { } }; -export const mockSuggestionTargetsObjectTwo: OpenaireSuggestionTarget = { +export const mockSuggestionTargetsObjectTwo: SuggestionTarget = { type: new ResourceType('suggestiontarget'), id: 'reciter:nhy567-9d6d-ty67-b905-fef0f8cae26', display: 'Digilio, Andrea', diff --git a/src/app/shared/mocks/reciter-suggestion.mock.ts b/src/app/shared/mocks/reciter-suggestion.mock.ts index ae1732808e..aceca72fc8 100644 --- a/src/app/shared/mocks/reciter-suggestion.mock.ts +++ b/src/app/shared/mocks/reciter-suggestion.mock.ts @@ -2,10 +2,10 @@ // REST Mock --------------------------------------------------------------------- // ------------------------------------------------------------------------------- -import { OpenaireSuggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; -import { SUGGESTION } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-objects.resource-type'; +import { Suggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; +import { SUGGESTION } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type'; -export const mockSuggestionPublicationOne: OpenaireSuggestion = { +export const mockSuggestionPublicationOne: Suggestion = { id: '24694772', display: 'publication one', source: 'reciter', @@ -107,7 +107,7 @@ export const mockSuggestionPublicationOne: OpenaireSuggestion = { } }; -export const mockSuggestionPublicationTwo: OpenaireSuggestion = { +export const mockSuggestionPublicationTwo: Suggestion = { id: '24694772', display: 'publication two', source: 'reciter', diff --git a/src/app/shared/mocks/openaire.mock.ts b/src/app/shared/mocks/suggestion.mock.ts similarity index 100% rename from src/app/shared/mocks/openaire.mock.ts rename to src/app/shared/mocks/suggestion.mock.ts diff --git a/src/app/suggestion-notifications/reciter-suggestions/selectors.ts b/src/app/suggestion-notifications/reciter-suggestions/selectors.ts index 6ba474bfd8..aa81ab4bc8 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/selectors.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/selectors.ts @@ -1,6 +1,6 @@ import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/store'; import { suggestionNotificationsSelector, SuggestionNotificationsState } from '../suggestion-notifications.reducer'; -import { OpenaireSuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; import {subStateSelector} from '../../submission/selectors'; @@ -10,7 +10,7 @@ import {subStateSelector} from '../../submission/selectors'; * @param {AppState} state Top level state. * @return {SuggestionNotificationsState} */ -const _getReciterSuggestionTargetState = createFeatureSelector('openaire'); +const _getReciterSuggestionTargetState = createFeatureSelector('suggestion-notifications'); // Reciter Suggestion Targets // ---------------------------------------------------------------------------- @@ -29,8 +29,8 @@ export function reciterSuggestionTargetStateSelector(): MemoizedSelector { - return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); +export function reciterSuggestionTargetObjectSelector(): MemoizedSelector { + return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); } /** @@ -81,7 +81,7 @@ export const getreciterSuggestionTargetTotalsSelector = createSelector(_getRecit /** * Returns Suggestion Targets for the current user. * @function getCurrentUserReciterSuggestionTargetSelector - * @return {OpenaireSuggestionTarget[]} + * @return {SuggestionTarget[]} */ export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts index 7a3312af0c..35599669ef 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-actions/suggestion-actions.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { OpenaireSuggestion } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; +import { Suggestion } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-list-element.component'; import { Collection } from '../../../core/shared/collection.model'; import { take } from 'rxjs/operators'; @@ -14,7 +14,7 @@ import { CreateItemParentSelectorComponent } from '../../../shared/dso-selector/ }) export class SuggestionActionsComponent { - @Input() object: OpenaireSuggestion; + @Input() object: Suggestion; @Input() isBulk = false; diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts index 7bd5fc6e1b..431749eaa5 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { fadeIn } from '../../../../shared/animations/fade'; -import { SuggestionEvidences } from '../../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; +import { SuggestionEvidences } from '../../../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; @Component({ selector: 'ds-suggestion-evidences', diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts index ca531655fc..a1ef454037 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component.ts @@ -3,12 +3,12 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { fadeIn } from '../../../shared/animations/fade'; -import { OpenaireSuggestion } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; +import { Suggestion } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; import { Item } from '../../../core/shared/item.model'; import { isNotEmpty } from '../../../shared/empty.util'; export interface SuggestionApproveAndImport { - suggestion: OpenaireSuggestion; + suggestion: Suggestion; collectionId: string; } @@ -20,7 +20,7 @@ export interface SuggestionApproveAndImport { }) export class SuggestionListElementComponent implements OnInit { - @Input() object: OpenaireSuggestion; + @Input() object: Suggestion; @Input() isSelected = false; diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts index 627b815554..09e4f6e20c 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; import { type } from '../../../shared/ngrx/type'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; /** * For each action type in an action group, make a simple @@ -66,7 +66,7 @@ export class RetrieveAllTargetsErrorAction implements Action { export class AddTargetAction implements Action { type = SuggestionTargetActionTypes.ADD_TARGETS; payload: { - targets: OpenaireSuggestionTarget[]; + targets: SuggestionTarget[]; totalPages: number; currentPage: number; totalElements: number; @@ -84,7 +84,7 @@ export class AddTargetAction implements Action { * @param totalElements * the total available Suggestion Targets */ - constructor(targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + constructor(targets: SuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { this.payload = { targets, totalPages, @@ -102,7 +102,7 @@ export class AddTargetAction implements Action { export class AddUserSuggestionsAction implements Action { type = SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS; payload: { - suggestionTargets: OpenaireSuggestionTarget[]; + suggestionTargets: SuggestionTarget[]; }; /** @@ -111,7 +111,7 @@ export class AddUserSuggestionsAction implements Action { * @param suggestionTargets * the user suggestions target */ - constructor(suggestionTargets: OpenaireSuggestionTarget[]) { + constructor(suggestionTargets: SuggestionTarget[]) { this.payload = { suggestionTargets }; } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts index 7cd882d01e..cfa168e2b8 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { distinctUntilChanged, take } from 'rxjs/operators'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { hasValue } from '../../../shared/empty.util'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SuggestionTargetsStateService } from './suggestion-targets.state.service'; @@ -39,7 +39,7 @@ export class SuggestionTargetsComponent implements OnInit { /** * The Suggestion Target list. */ - public targets$: Observable; + public targets$: Observable; /** * The total number of Suggestion Targets. */ @@ -144,7 +144,7 @@ export class SuggestionTargetsComponent implements OnInit { }); } - public getTargetUuid(target: OpenaireSuggestionTarget) { + public getTargetUuid(target: SuggestionTarget) { return this.suggestionService.getTargetUuid(target); } } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index 01b305bbd4..9c595a1e0d 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -18,7 +18,7 @@ import { PaginatedList } from '../../../core/data/paginated-list.model'; import { SuggestionsService } from '../suggestions.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { AuthActionTypes, RetrieveAuthenticatedEpersonSuccessAction } from '../../../core/auth/auth.actions'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { EPerson } from '../../../core/eperson/models/eperson.model'; /** @@ -38,7 +38,7 @@ export class SuggestionTargetsEffects { action.payload.elementsPerPage, action.payload.currentPage ).pipe( - map((targets: PaginatedList) => + map((targets: PaginatedList) => new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements) ), catchError((error: Error) => { @@ -68,7 +68,7 @@ export class SuggestionTargetsEffects { ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS), switchMap((action: RetrieveAuthenticatedEpersonSuccessAction) => { return this.suggestionsService.retrieveCurrentUserSuggestions(action.payload).pipe( - map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)) + map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)) ); })); @@ -83,7 +83,7 @@ export class SuggestionTargetsEffects { switchMap((userId: string) => { return this.suggestionsService.retrieveCurrentUserSuggestions(userId) .pipe( - map((suggestionTargets: OpenaireSuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), + map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)), catchError((errors) => of(errors)) ); }), diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts index b0b551dfa9..2d315e6995 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts @@ -1,17 +1,17 @@ import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; /** * The interface representing the OpenAIRE suggestion targets state. */ export interface SuggestionTargetState { - targets: OpenaireSuggestionTarget[]; + targets: SuggestionTarget[]; processing: boolean; loaded: boolean; totalPages: number; currentPage: number; totalElements: number; - currentUserTargets: OpenaireSuggestionTarget[]; + currentUserTargets: SuggestionTarget[]; currentUserTargetsVisited: boolean; } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts index 164750d221..97792df6af 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts @@ -13,7 +13,7 @@ import { isreciterSuggestionTargetProcessingSelector, reciterSuggestionTargetObjectSelector } from '../selectors'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { ClearSuggestionTargetsAction, MarkUserSuggestionsAsVisitedAction, @@ -40,7 +40,7 @@ export class SuggestionTargetsStateService { * @return Observable * The list of Reciter Suggestion Targets. */ - public getReciterSuggestionTargets(): Observable { + public getReciterSuggestionTargets(): Observable { return this.store.pipe(select(reciterSuggestionTargetObjectSelector())); } @@ -127,7 +127,7 @@ export class SuggestionTargetsStateService { * @return Observable * The Reciter Suggestion Targets object. */ - public getCurrentUserSuggestionTargets(): Observable { + public getCurrentUserSuggestionTargets(): Observable { return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts index 19f94c433d..23b34c4de9 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { TranslateService } from '@ngx-translate/core'; import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -18,7 +18,7 @@ export class SuggestionsNotificationComponent implements OnInit { /** * The user suggestion targets. */ - suggestionsRD$: Observable; + suggestionsRD$: Observable; constructor( private translateService: TranslateService, @@ -35,7 +35,7 @@ export class SuggestionsNotificationComponent implements OnInit { * Interpolated params to build the notification suggestions notification. * @param suggestionTarget */ - public getNotificationSuggestionInterpolation(suggestionTarget: OpenaireSuggestionTarget): any { + public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any { return this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget); } diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts index 9478ed2d1d..0195f074ae 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts @@ -4,7 +4,7 @@ import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion- import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { SuggestionsService } from '../suggestions.service'; import { takeUntil } from 'rxjs/operators'; -import { OpenaireSuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { isNotEmpty } from '../../../shared/empty.util'; import { combineLatest, Subject } from 'rxjs'; @@ -38,7 +38,7 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy { ]).pipe(takeUntil(notifier)).subscribe(([suggestions, visited]) => { if (isNotEmpty(suggestions)) { if (!visited) { - suggestions.forEach((suggestionTarget: OpenaireSuggestionTarget) => this.showNotificationForNewSuggestions(suggestionTarget)); + suggestions.forEach((suggestionTarget: SuggestionTarget) => this.showNotificationForNewSuggestions(suggestionTarget)); this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction(); notifier.next(null); notifier.complete(); @@ -52,7 +52,7 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy { * @param suggestionTarget * @private */ - private showNotificationForNewSuggestions(suggestionTarget: OpenaireSuggestionTarget): void { + 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); diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts index c4a2fa4b4b..be43285df2 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts @@ -3,11 +3,11 @@ import { Injectable } from '@angular/core'; import { of, forkJoin, Observable } from 'rxjs'; import { catchError, map, mergeMap, take } from 'rxjs/operators'; -import { OpenaireSuggestionsDataService } from '../../core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service'; +import { SuggestionsDataService } from '../../core/suggestion-notifications/reciter-suggestions/suggestions-data.service'; 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 { OpenaireSuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; import { AuthService } from '../../core/auth/auth.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; @@ -19,7 +19,7 @@ import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../../core/shared/operators'; -import { OpenaireSuggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; +import { Suggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NoContent } from '../../core/shared/NoContent.model'; @@ -43,12 +43,12 @@ export class SuggestionsService { * Initialize the service variables. * @param {AuthService} authService * @param {ResearcherProfileService} researcherProfileService - * @param {OpenaireSuggestionsDataService} suggestionsDataService + * @param {SuggestionsDataService} suggestionsDataService */ constructor( private authService: AuthService, private researcherProfileService: ResearcherProfileService, - private suggestionsDataService: OpenaireSuggestionsDataService, + private suggestionsDataService: SuggestionsDataService, private translateService: TranslateService ) { } @@ -65,7 +65,7 @@ export class SuggestionsService { * @return Observable> * The list of Suggestion Targets. */ - public getTargets(source, elementsPerPage, currentPage): Observable> { + public getTargets(source, elementsPerPage, currentPage): Observable> { const sortOptions = new SortOptions('display', SortDirection.ASC); const findListOptions: FindListOptions = { @@ -77,7 +77,7 @@ export class SuggestionsService { return this.suggestionsDataService.getTargets(source, findListOptions).pipe( getFinishedRemoteData(), take(1), - map((rd: RemoteData>) => { + map((rd: RemoteData>) => { if (rd.hasSucceeded) { return rd.payload; } else { @@ -98,10 +98,10 @@ export class SuggestionsService { * The page number to retrieve * @param sortOptions * The sort options - * @return Observable>> + * @return Observable>> * The list of Suggestion. */ - public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable> { + public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable> { const [source, target] = targetId.split(':'); const findListOptions: FindListOptions = { @@ -145,7 +145,7 @@ export class SuggestionsService { * @param userUuid * The EPerson id for which to retrieve suggestion targets */ - public retrieveCurrentUserSuggestions(userUuid: string): Observable { + public retrieveCurrentUserSuggestions(userUuid: string): Observable { return this.researcherProfileService.findById(userUuid).pipe( getFirstSucceededRemoteDataPayload(), mergeMap((profile: ResearcherProfile) => { @@ -173,7 +173,7 @@ export class SuggestionsService { * @private */ public approveAndImport(workspaceitemService: WorkspaceitemDataService, - suggestion: OpenaireSuggestion, + suggestion: Suggestion, collectionId: string): Observable { const resolvedCollectionId = this.resolveCollectionId(suggestion, collectionId); @@ -201,10 +201,10 @@ export class SuggestionsService { * @param collectionId the collectionId */ public approveAndImportMultiple(workspaceitemService: WorkspaceitemDataService, - suggestions: OpenaireSuggestion[], + suggestions: Suggestion[], collectionId: string): Observable { - return forkJoin(suggestions.map((suggestion: OpenaireSuggestion) => + return forkJoin(suggestions.map((suggestion: Suggestion) => this.approveAndImport(workspaceitemService, suggestion, collectionId))) .pipe(map((results: WorkspaceItem[]) => { return { @@ -218,8 +218,8 @@ export class SuggestionsService { * Perform a bulk notMine operation. * @param suggestions the array containing the suggestions */ - public notMineMultiple(suggestions: OpenaireSuggestion[]): Observable { - return forkJoin(suggestions.map((suggestion: OpenaireSuggestion) => this.notMine(suggestion.id))) + public notMineMultiple(suggestions: Suggestion[]): Observable { + return forkJoin(suggestions.map((suggestion: Suggestion) => this.notMine(suggestion.id))) .pipe(map((results: RemoteData[]) => { return { success: results.filter((result) => result != null).length, @@ -234,7 +234,7 @@ export class SuggestionsService { * @param target * @return the researchUuid */ - public getTargetUuid(target: OpenaireSuggestionTarget): string { + public getTargetUuid(target: SuggestionTarget): string { const tokens = target.id.split(':'); return tokens.length === 2 ? tokens[1] : null; } @@ -243,7 +243,7 @@ export class SuggestionsService { * Interpolated params to build the notification suggestions notification. * @param suggestionTarget */ - public getNotificationSuggestionInterpolation(suggestionTarget: OpenaireSuggestionTarget): any { + public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any { return { count: suggestionTarget.total, source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)), @@ -266,7 +266,7 @@ export class SuggestionsService { * @param suggestion * @param collectionId */ - public resolveCollectionId(suggestion: OpenaireSuggestion, collectionId): string { + public resolveCollectionId(suggestion: Suggestion, collectionId): string { if (hasValue(collectionId)) { return collectionId; } @@ -280,13 +280,13 @@ export class SuggestionsService { * in the configuration. * @param suggestions */ - public isCollectionFixed(suggestions: OpenaireSuggestion[]): boolean { + public isCollectionFixed(suggestions: Suggestion[]): boolean { return this.getFixedCollectionIds(suggestions).length === 1; } - private getFixedCollectionIds(suggestions: OpenaireSuggestion[]): string[] { + private getFixedCollectionIds(suggestions: Suggestion[]): string[] { const collectionIds = {}; - suggestions.forEach((suggestion: OpenaireSuggestion) => { + suggestions.forEach((suggestion: Suggestion) => { const conf = environment.suggestion.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source); if (hasValue(conf)) { collectionIds[conf.collectionId] = true; diff --git a/src/app/suggestion-notifications/selectors.ts b/src/app/suggestion-notifications/selectors.ts index c5947e3196..a20e900025 100644 --- a/src/app/suggestion-notifications/selectors.ts +++ b/src/app/suggestion-notifications/selectors.ts @@ -12,7 +12,7 @@ import { QualityAssuranceSourceObject } from '../core/suggestion-notifications/q * @param {AppState} state Top level state. * @return {SuggestionNotificationsState} */ -const _getNotificationsState = createFeatureSelector('notifications'); +const _getNotificationsState = createFeatureSelector('suggestion-notifications'); // Quality Assurance topics // ---------------------------------------------------------------------------- diff --git a/src/app/suggestion-notifications/suggestion-notifications.module.ts b/src/app/suggestion-notifications/suggestion-notifications.module.ts index b69427d70d..2b2e41ece3 100644 --- a/src/app/suggestion-notifications/suggestion-notifications.module.ts +++ b/src/app/suggestion-notifications/suggestion-notifications.module.ts @@ -34,13 +34,13 @@ import {SuggestionActionsComponent} from './reciter-suggestions/suggestion-actio import {SuggestionTargetsComponent} from './reciter-suggestions/suggestion-targets/suggestion-targets.component'; import {SuggestionTargetsStateService} from './reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; import {SuggestionsService} from './reciter-suggestions/suggestions.service'; -import {OpenaireSuggestionsDataService} from '../core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service'; +import {SuggestionsDataService} from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service'; const MODULES = [ CommonModule, SharedModule, CoreModule.forRoot(), - StoreModule.forFeature('notifications', suggestionNotificationsReducers, storeModuleConfig as StoreConfig), + StoreModule.forFeature('suggestion-notifications', suggestionNotificationsReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(suggestionNotificationsEffects), TranslateModule ]; @@ -72,7 +72,7 @@ const PROVIDERS = [ QualityAssuranceEventRestService, SuggestionTargetsStateService, SuggestionsService, - OpenaireSuggestionsDataService + SuggestionsDataService ]; @NgModule({ diff --git a/src/app/suggestion-notifications/suggestion-notifications.reducer.ts b/src/app/suggestion-notifications/suggestion-notifications.reducer.ts index cdd072bb39..bab0984304 100644 --- a/src/app/suggestion-notifications/suggestion-notifications.reducer.ts +++ b/src/app/suggestion-notifications/suggestion-notifications.reducer.ts @@ -21,4 +21,4 @@ export const suggestionNotificationsReducers: ActionReducerMap('notifications'); +export const suggestionNotificationsSelector = createFeatureSelector('suggestion-notifications'); diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts index aef9332605..61e66a99cf 100644 --- a/src/app/suggestions-page/suggestions-page.component.spec.ts +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -9,9 +9,9 @@ import { of as observableOf } from 'rxjs'; import { SuggestionsPageComponent } from './suggestions-page.component'; import { SuggestionListElementComponent } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; import { SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service'; -import { getMockSuggestionNotificationsStateService, getMockSuggestionsService } from '../shared/mocks/openaire.mock'; +import { getMockSuggestionNotificationsStateService, getMockSuggestionsService } from '../shared/mocks/suggestion.mock'; import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model'; -import { OpenaireSuggestion } from '../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; +import { Suggestion } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; import { mockSuggestionPublicationOne, mockSuggestionPublicationTwo } from '../shared/mocks/reciter-suggestion.mock'; import { SuggestionEvidencesComponent } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; import { ObjectKeysPipe } from '../shared/utils/object-keys-pipe'; @@ -38,7 +38,7 @@ describe('SuggestionPageComponent', () => { let scheduler: TestScheduler; const mockSuggestionsService = getMockSuggestionsService(); const mockSuggestionsTargetStateService = getMockSuggestionNotificationsStateService(); - const suggestionTargetsList: PaginatedList = buildPaginatedList(new PageInfo(), [mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + const suggestionTargetsList: PaginatedList = buildPaginatedList(new PageInfo(), [mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); const router = new RouterStub(); const routeStub = { data: observableOf({ diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index e8e76ff606..b4ca0108d5 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -11,8 +11,8 @@ import { RemoteData } from '../core/data/remote-data'; import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { SuggestionBulkResult, SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { OpenaireSuggestion } from '../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion.model'; -import { OpenaireSuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { Suggestion } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion.model'; +import { SuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; import { AuthService } from '../core/auth/auth.service'; import { SuggestionApproveAndImport } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; @@ -56,18 +56,18 @@ export class SuggestionsPageComponent implements OnInit { /** * A list of remote data objects of suggestions */ - suggestionsRD$: BehaviorSubject> = new BehaviorSubject>({} as any); + suggestionsRD$: BehaviorSubject> = new BehaviorSubject>({} as any); - targetRD$: Observable>; + targetRD$: Observable>; targetId$: Observable; - suggestionTarget: OpenaireSuggestionTarget; + suggestionTarget: SuggestionTarget; suggestionId: any; suggestionSource: any; researcherName: any; researcherUuid: any; - selectedSuggestions: { [id: string]: OpenaireSuggestion } = {}; + selectedSuggestions: { [id: string]: Suggestion } = {}; isBulkOperationPending = false; constructor( @@ -85,17 +85,17 @@ export class SuggestionsPageComponent implements OnInit { ngOnInit(): void { this.targetRD$ = this.route.data.pipe( - map((data: Data) => data.suggestionTargets as RemoteData), + map((data: Data) => data.suggestionTargets as RemoteData), redirectOn4xx(this.router, this.authService) ); this.targetId$ = this.targetRD$.pipe( getFirstSucceededRemoteDataPayload(), - map((target: OpenaireSuggestionTarget) => target.id) + map((target: SuggestionTarget) => target.id) ); this.targetRD$.pipe( getFirstSucceededRemoteDataPayload() - ).subscribe((suggestionTarget: OpenaireSuggestionTarget) => { + ).subscribe((suggestionTarget: SuggestionTarget) => { this.suggestionTarget = suggestionTarget; this.suggestionId = suggestionTarget.id; this.researcherName = suggestionTarget.display; @@ -135,7 +135,7 @@ export class SuggestionsPageComponent implements OnInit { ); }), take(1) - ).subscribe((results: PaginatedList) => { + ).subscribe((results: PaginatedList) => { this.processing$.next(false); this.suggestionsRD$.next(results); this.suggestionService.clearSuggestionRequests(); @@ -232,7 +232,7 @@ export class SuggestionsPageComponent implements OnInit { * @param object the suggestions * @param selected the new selected value for the suggestion */ - onSelected(object: OpenaireSuggestion, selected: boolean) { + onSelected(object: Suggestion, selected: boolean) { if (selected) { this.selectedSuggestions[object.id] = object; } else { @@ -244,7 +244,7 @@ export class SuggestionsPageComponent implements OnInit { * When Toggle Select All occurs. * @param suggestions all the visible suggestions inside the page */ - onToggleSelectAll(suggestions: OpenaireSuggestion[]) { + onToggleSelectAll(suggestions: Suggestion[]) { if ( this.getSelectedSuggestionsCount() > 0) { this.selectedSuggestions = {}; } else { @@ -265,7 +265,7 @@ export class SuggestionsPageComponent implements OnInit { * Return true if all the suggestion are configured with the same fixed collection in the configuration. * @param suggestions */ - isCollectionFixed(suggestions: OpenaireSuggestion[]): boolean { + isCollectionFixed(suggestions: Suggestion[]): boolean { return this.suggestionService.isCollectionFixed(suggestions); } diff --git a/src/app/suggestions-page/suggestions-page.module.ts b/src/app/suggestions-page/suggestions-page.module.ts index 726af26c0f..a3f24f3468 100644 --- a/src/app/suggestions-page/suggestions-page.module.ts +++ b/src/app/suggestions-page/suggestions-page.module.ts @@ -5,7 +5,7 @@ import { SuggestionsPageComponent } from './suggestions-page.component'; import { SharedModule } from '../shared/shared.module'; import { SuggestionsPageRoutingModule } from './suggestions-page-routing.module'; import { SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service'; -import { OpenaireSuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service'; +import { SuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service'; import {SuggestionNotificationsModule} from '../suggestion-notifications/suggestion-notifications.module'; @NgModule({ @@ -17,7 +17,7 @@ import {SuggestionNotificationsModule} from '../suggestion-notifications/suggest SuggestionsPageRoutingModule ], providers: [ - OpenaireSuggestionsDataService, + SuggestionsDataService, SuggestionsService ] }) diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index cf22b04aa6..6cc89a72d4 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -6,15 +6,15 @@ import { find } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; import { hasValue } from '../shared/empty.util'; -import { OpenaireSuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/openaire-suggestions-data.service'; -import { OpenaireSuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service'; +import { SuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; /** * This class represents a resolver that requests a specific collection before the route is activated */ @Injectable() -export class SuggestionsPageResolver implements Resolve> { - constructor(private suggestionsDataService: OpenaireSuggestionsDataService) { +export class SuggestionsPageResolver implements Resolve> { + constructor(private suggestionsDataService: SuggestionsDataService) { } /** @@ -24,7 +24,7 @@ export class SuggestionsPageResolver implements Resolve> Emits the found collection based on the parameters in the current route, * or an error if something went wrong */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), ); From 78d0bdd336f65204bafca66f1c1734f62906a042 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 18 Aug 2023 14:18:41 +0200 Subject: [PATCH 005/159] 104938 Implementation and tests (WIP) --- ...llection-source-controls.component.spec.ts | 8 +- .../collection-source-controls.component.ts | 194 +++++++++++------- .../processes/process-data.service.spec.ts | 113 ++++++++++ .../data/processes/process-data.service.ts | 44 +++- .../detail/process-detail.component.spec.ts | 124 +++++------ .../detail/process-detail.component.ts | 139 +++++++------ 6 files changed, 418 insertions(+), 204 deletions(-) diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts index 3eb83ebe8a..37a5d8642d 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts @@ -88,7 +88,7 @@ describe('CollectionSourceControlsComponent', () => { invoke: createSuccessfulRemoteDataObject$(process), }); processDataService = jasmine.createSpyObj('processDataService', { - findById: createSuccessfulRemoteDataObject$(process), + notifyOnCompletion: createSuccessfulRemoteDataObject$(process), }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findByHref: createSuccessfulRemoteDataObject$(bitstream), @@ -137,7 +137,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)}, ], []); - expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false); + expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); expect(bitstreamService.findByHref).toHaveBeenCalledWith(process._links.output.href); expect(notificationsService.info).toHaveBeenCalledWith(jasmine.anything() as any, 'Script text'); }); @@ -151,7 +151,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-r', value: null}, {name: '-c', value: collection.uuid}, ], []); - expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false); + expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); expect(notificationsService.success).toHaveBeenCalled(); }); }); @@ -164,7 +164,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-o', value: null}, {name: '-c', value: collection.uuid}, ], []); - expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false); + expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); expect(notificationsService.success).toHaveBeenCalled(); }); }); diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts index 7113c25e9f..e8c3666da0 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts @@ -95,36 +95,55 @@ export class CollectionSourceControlsComponent implements OnDestroy { }), // filter out responses that aren't successful since the pinging of the process only needs to happen when the invocation was successful. filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)), - getAllCompletedRemoteData(), - filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - map((rd) => rd.payload), - hasValueOperator(), + switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + map((rd) => rd.payload) ).subscribe((process: Process) => { - if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // Ping the current process state every 5s - setTimeout(() => { - this.requestService.setStaleByHrefSubstring(process._links.self.href); - }, 5000); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed')); - this.testConfigRunning$.next(false); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - this.bitstreamService.findByHref(process._links.output.href).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => { - this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => { - const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1') - .replaceAll('The script has started', '') - .replaceAll('The script has completed', ''); - this.notificationsService.info(this.translateService.get('collection.source.controls.test.completed'), output); - }); - }); - this.testConfigRunning$.next(false); - } + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed')); + this.testConfigRunning$.next(false); } - )); + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + this.bitstreamService.findByHref(process._links.output.href).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => { + this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => { + const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1') + .replaceAll('The script has started', '') + .replaceAll('The script has completed', ''); + this.notificationsService.info(this.translateService.get('collection.source.controls.test.completed'), output); + }); + }); + this.testConfigRunning$.next(false); + } + })); + + // getAllCompletedRemoteData(), + // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), + // map((rd) => rd.payload), + // hasValueOperator(), + // ).subscribe((process: Process) => { + // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && + // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { + // // Ping the current process state every 5s + // setTimeout(() => { + // this.requestService.setStaleByHrefSubstring(process._links.self.href); + // }, 5000); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + // this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed')); + // this.testConfigRunning$.next(false); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + // this.bitstreamService.findByHref(process._links.output.href).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => { + // this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => { + // const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1') + // .replaceAll('The script has started', '') + // .replaceAll('The script has completed', ''); + // this.notificationsService.info(this.translateService.get('collection.source.controls.test.completed'), output); + // }); + // }); + // this.testConfigRunning$.next(false); + // } + // } + // )); } /** @@ -147,31 +166,44 @@ export class CollectionSourceControlsComponent implements OnDestroy { } }), filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)), - getAllCompletedRemoteData(), - filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - map((rd) => rd.payload), - hasValueOperator(), + switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + map((rd) => rd.payload) ).subscribe((process) => { - if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // Ping the current process state every 5s - setTimeout(() => { - this.requestService.setStaleByHrefSubstring(process._links.self.href); - this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - }, 5000); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed')); - this.importRunning$.next(false); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed')); - this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - this.importRunning$.next(false); - } + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed')); + this.importRunning$.next(false); } - )); + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed')); + this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + this.importRunning$.next(false); + } + })); + + // getAllCompletedRemoteData(), + // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), + // map((rd) => rd.payload), + // hasValueOperator(), + // ).subscribe((process) => { + // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && + // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { + // // Ping the current process state every 5s + // setTimeout(() => { + // this.requestService.setStaleByHrefSubstring(process._links.self.href); + // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + // }, 5000); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + // this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed')); + // this.importRunning$.next(false); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + // this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed')); + // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + // this.importRunning$.next(false); + // } + // } + // )); } /** @@ -194,31 +226,45 @@ export class CollectionSourceControlsComponent implements OnDestroy { } }), filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)), - getAllCompletedRemoteData(), - filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - map((rd) => rd.payload), - hasValueOperator(), + switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + map((rd) => rd.payload) ).subscribe((process) => { - if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // Ping the current process state every 5s - setTimeout(() => { - this.requestService.setStaleByHrefSubstring(process._links.self.href); - this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - }, 5000); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed')); - this.reImportRunning$.next(false); - } - if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - this.notificationsService.success(this.translateService.get('collection.source.controls.reset.completed')); - this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - this.reImportRunning$.next(false); - } + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed')); + this.reImportRunning$.next(false); } - )); + if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + this.notificationsService.success(this.translateService.get('collection.source.controls.reset.completed')); + this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + this.reImportRunning$.next(false); + } + })); + + // switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)), + // getAllCompletedRemoteData(), + // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), + // map((rd) => rd.payload), + // hasValueOperator(), + // ).subscribe((process) => { + // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && + // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { + // // Ping the current process state every 5s + // setTimeout(() => { + // this.requestService.setStaleByHrefSubstring(process._links.self.href); + // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + // }, 5000); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { + // this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed')); + // this.reImportRunning$.next(false); + // } + // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { + // this.notificationsService.success(this.translateService.get('collection.source.controls.reset.completed')); + // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); + // this.reImportRunning$.next(false); + // } + // } + // )); } ngOnDestroy(): void { diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index 88e5bd5791..6e7ce51502 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -9,6 +9,21 @@ import { testFindAllDataImplementation } from '../base/find-all-data.spec'; import { ProcessDataService } from './process-data.service'; import { testDeleteDataImplementation } from '../base/delete-data.spec'; +import { cold } from 'jasmine-marbles'; +import { waitForAsync, TestBed } from '@angular/core/testing'; +import { RequestService } from '../request.service'; +import { RemoteData } from '../remote-data'; +import { RequestEntryState } from '../request-entry-state.model'; +import { Process } from '../../../process-page/processes/process.model'; +import { ProcessStatus } from '../../../process-page/processes/process-status.model'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { CoreModule } from '../../core.module'; +import { ReducerManager } from '@ngrx/store'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; +import { BitstreamFormatDataService } from '../bitstream-format-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; describe('ProcessDataService', () => { describe('composition', () => { @@ -16,4 +31,102 @@ describe('ProcessDataService', () => { testFindAllDataImplementation(initService); testDeleteDataImplementation(initService); }); + + let requestService; + let processDataService; + let remoteDataBuildService; + + describe('notifyOnCompletion', () => { + beforeEach(waitForAsync(() => { + requestService = jasmine.createSpyObj('requestService', ['setStaleByHrefSubstring']); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + ProcessDataService, + { provide: RequestService, useValue: requestService }, + { provide: RemoteDataBuildService, useValue: null }, + { provide: ObjectCacheService, useValue: null }, + { provide: ReducerManager, useValue: null }, + { provide: HALEndpointService, useValue: null }, + { provide: DSOChangeAnalyzer, useValue: null }, + { provide: BitstreamFormatDataService, useValue: null }, + { provide: NotificationsService, useValue: null }, + ] + }); + + processDataService = TestBed.inject(ProcessDataService); + })); + + it('TODO', () => { + let completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; + + spyOn(processDataService, 'findById').and.returnValue( + cold('(c|)', { + 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + }) + ); + + let process$ = processDataService.notifyOnCompletion('instantly'); + process$.subscribe((rd) => { + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled(); + }); + expect(process$).toBeObservable(cold('(c|)', { + 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + })); + }); + + // it('TODO2', () => { + // let completedProcess = new Process(); + // completedProcess.processStatus = ProcessStatus.COMPLETED; + + // spyOn(processDataService, 'findById').and.returnValue( + // cold('p 150ms (c|)', { + // 'p': new RemoteData(0, 0, 0, RequestEntryState., null, completedProcess), + // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + // }) + // ); + + // let process$ = processDataService.notifyOnCompletion('foo', 100); + // expect(process$).toBeObservable(cold('---(c|)', { + // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + // })); + // process$.subscribe((rd) => { + // expect(processDataService.findById).toHaveBeenCalledTimes(1); + // expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled(); + // }); + // }); + }); + }); + +// /** +// * Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it +// */ +// export function testFindAllDataImplementation(serviceFactory: () => FindAllData) { +// let service; +// +// describe('FindAllData implementation', () => { +// const OPTIONS = Object.assign(new FindListOptions(), { elementsPerPage: 10, currentPage: 3 }); +// const FOLLOWLINKS = [ +// followLink('test'), +// followLink('something'), +// ]; +// +// beforeAll(() => { +// service = serviceFactory(); +// (service as any).findAllData = jasmine.createSpyObj('findAllData', { +// findAll: 'TEST findAll', +// }); +// }); +// +// it('should handle calls to findAll', () => { +// const out: any = service.findAll(OPTIONS, false, true, ...FOLLOWLINKS); +// +// expect((service as any).findAllData.findAll).toHaveBeenCalledWith(OPTIONS, false, true, ...FOLLOWLINKS); +// expect(out).toBe('TEST findAll'); +// }); +// }); +// } diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 3bf34eb650..f550407a59 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -6,25 +6,28 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { Process } from '../../../process-page/processes/process.model'; import { PROCESS } from '../../../process-page/processes/process.resource-type'; import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { switchMap, filter, take } from 'rxjs/operators'; import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; import { RemoteData } from '../remote-data'; import { BitstreamDataService } from '../bitstream-data.service'; import { IdentifiableDataService } from '../base/identifiable-data.service'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; import { FindListOptions } from '../find-list-options.model'; import { dataService } from '../base/data-service.decorator'; import { DeleteData, DeleteDataImpl } from '../base/delete-data'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NoContent } from '../../shared/NoContent.model'; +import { getAllCompletedRemoteData } from '../../shared/operators'; +import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; @Injectable() @dataService(PROCESS) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { private findAllData: FindAllData; private deleteData: DeleteData; + protected activelyBeingPolled: Set = new Set(); constructor( protected requestService: RequestService, @@ -101,4 +104,41 @@ export class ProcessDataService extends IdentifiableDataService impleme public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { return this.deleteData.deleteByHref(href, copyVirtualMetadata); } + + // TODO + public notifyOnCompletion(processId: string, pollingIntervalInMs = 5000): Observable> { + const process$ = this.findById(processId, false, true, followLink('script')) + .pipe( + getAllCompletedRemoteData(), + ); + + // TODO: this is horrible + const statusIs = (process: Process, status: ProcessStatus) => + process.processStatus === status; + + // If we have to wait too long for the result, we should mark the result as stale. + // However, we should make sure this happens only once (in case there are multiple observers waiting + // for the result). + if (!this.activelyBeingPolled.has(processId)) { + this.activelyBeingPolled.add(processId); + + // Create a subscription that marks the data as stale if the polling interval time has been exceeded. + const sub = process$.subscribe((rd) => { + const process = rd.payload; + if (statusIs(process, ProcessStatus.COMPLETED) || statusIs(process, ProcessStatus.FAILED)) { + this.activelyBeingPolled.delete(processId); + sub.unsubscribe(); + } else { + setTimeout(() => { + this.requestService.setStaleByHrefSubstring(process._links.self.href); + }, pollingIntervalInMs); + } + }); + } + + return process$.pipe( + filter(rd => statusIs(rd.payload, ProcessStatus.COMPLETED) || statusIs(rd.payload, ProcessStatus.FAILED)), + take(1) + ); + } } diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index 9552f9a092..68b97d0e5c 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -273,92 +273,92 @@ describe('ProcessDetailComponent', () => { }); }); - describe('refresh counter', () => { - const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter')); + // describe('refresh counter', () => { + // const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter')); - describe('if process is completed', () => { - beforeEach(() => { - process.processStatus = ProcessStatus.COMPLETED; - route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); - }); + // describe('if process is completed', () => { + // beforeEach(() => { + // process.processStatus = ProcessStatus.COMPLETED; + // route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); + // }); - it('should not show', () => { - spyOn(component, 'startRefreshTimer'); + // it('should not show', () => { + // spyOn(component, 'startRefreshTimer'); - const refreshCounter = queryRefreshCounter(); - expect(refreshCounter).toBeNull(); + // const refreshCounter = queryRefreshCounter(); + // expect(refreshCounter).toBeNull(); - expect(component.startRefreshTimer).not.toHaveBeenCalled(); - }); - }); + // expect(component.startRefreshTimer).not.toHaveBeenCalled(); + // }); + // }); - describe('if process is not finished', () => { - beforeEach(() => { - process.processStatus = ProcessStatus.RUNNING; - route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); - fixture.detectChanges(); - component.stopRefreshTimer(); - }); + // describe('if process is not finished', () => { + // beforeEach(() => { + // process.processStatus = ProcessStatus.RUNNING; + // route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); + // fixture.detectChanges(); + // component.stopRefreshTimer(); + // }); - it('should call startRefreshTimer', () => { - spyOn(component, 'startRefreshTimer'); + // it('should call startRefreshTimer', () => { + // spyOn(component, 'startRefreshTimer'); - component.ngOnInit(); - fixture.detectChanges(); // subscribe to process observable with async pipe + // component.ngOnInit(); + // fixture.detectChanges(); // subscribe to process observable with async pipe - expect(component.startRefreshTimer).toHaveBeenCalled(); - }); + // expect(component.startRefreshTimer).toHaveBeenCalled(); + // }); - it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => { - spyOn(component, 'refresh'); - spyOn(component, 'stopRefreshTimer'); + // it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => { + // spyOn(component, 'refresh'); + // spyOn(component, 'stopRefreshTimer'); - process.processStatus = ProcessStatus.COMPLETED; - // set findbyId to return a completed process - (processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process))); + // process.processStatus = ProcessStatus.COMPLETED; + // // set findbyId to return a completed process + // (processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process))); - component.ngOnInit(); - fixture.detectChanges(); // subscribe to process observable with async pipe + // component.ngOnInit(); + // fixture.detectChanges(); // subscribe to process observable with async pipe - expect(component.refresh).not.toHaveBeenCalled(); + // expect(component.refresh).not.toHaveBeenCalled(); - expect(component.refreshCounter$.value).toBe(0); + // expect(component.refreshCounter$.value).toBe(0); - tick(1001); // 1 second + 1 ms by the setTimeout - expect(component.refreshCounter$.value).toBe(5); // 5 - 0 + // tick(1001); // 1 second + 1 ms by the setTimeout + // expect(component.refreshCounter$.value).toBe(5); // 5 - 0 - tick(2001); // 2 seconds + 1 ms by the setTimeout - expect(component.refreshCounter$.value).toBe(3); // 5 - 2 + // tick(2001); // 2 seconds + 1 ms by the setTimeout + // expect(component.refreshCounter$.value).toBe(3); // 5 - 2 - tick(2001); // 2 seconds + 1 ms by the setTimeout - expect(component.refreshCounter$.value).toBe(1); // 3 - 2 + // tick(2001); // 2 seconds + 1 ms by the setTimeout + // expect(component.refreshCounter$.value).toBe(1); // 3 - 2 - tick(1001); // 1 second + 1 ms by the setTimeout - expect(component.refreshCounter$.value).toBe(0); // 1 - 1 + // tick(1001); // 1 second + 1 ms by the setTimeout + // expect(component.refreshCounter$.value).toBe(0); // 1 - 1 - tick(1000); // 1 second + // tick(1000); // 1 second - expect(component.refresh).toHaveBeenCalledTimes(1); - expect(component.stopRefreshTimer).toHaveBeenCalled(); + // expect(component.refresh).toHaveBeenCalledTimes(1); + // expect(component.stopRefreshTimer).toHaveBeenCalled(); - expect(component.refreshCounter$.value).toBe(0); + // expect(component.refreshCounter$.value).toBe(0); - tick(1001); // 1 second + 1 ms by the setTimeout - // startRefreshTimer not called again - expect(component.refreshCounter$.value).toBe(0); + // tick(1001); // 1 second + 1 ms by the setTimeout + // // startRefreshTimer not called again + // expect(component.refreshCounter$.value).toBe(0); - discardPeriodicTasks(); // discard any periodic tasks that have not yet executed - })); + // discardPeriodicTasks(); // discard any periodic tasks that have not yet executed + // })); - it('should show if refreshCounter is different from 0', () => { - component.refreshCounter$.next(1); - fixture.detectChanges(); + // it('should show if refreshCounter is different from 0', () => { + // component.refreshCounter$.next(1); + // fixture.detectChanges(); - const refreshCounter = queryRefreshCounter(); - expect(refreshCounter).not.toBeNull(); - }); + // const refreshCounter = queryRefreshCounter(); + // expect(refreshCounter).not.toBeNull(); + // }); - }); + // }); - }); + // }); }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index a379dfe337..18992eae2f 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; -import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, interval, Observable, shareReplay, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { finalize, map, switchMap, take, tap } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @@ -26,8 +26,6 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { getProcessListRoute } from '../process-page-routing.paths'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'ds-process-detail', @@ -36,7 +34,7 @@ import { isPlatformBrowser } from '@angular/common'; /** * A component displaying detailed information about a DSpace Process */ -export class ProcessDetailComponent implements OnInit, OnDestroy { +export class ProcessDetailComponent implements OnInit { /** * The AlertType enumeration @@ -107,18 +105,22 @@ export class ProcessDetailComponent implements OnInit, OnDestroy { * Display a 404 if the process doesn't exist */ ngOnInit(): void { - this.processRD$ = this.route.data.pipe( - map((data) => { - if (isPlatformBrowser(this.platformId)) { - if (!this.isProcessFinished(data.process.payload)) { - this.startRefreshTimer(); - } - } + // this.processRD$ = this.route.data.pipe( + // map((data) => { + // if (isPlatformBrowser(this.platformId)) { + // if (!this.isProcessFinished(data.process.payload)) { + // this.startRefreshTimer(); + // } + // } - return data.process as RemoteData; - }), - redirectOn4xx(this.router, this.authService), - shareReplay(1) + // return data.process as RemoteData; + // }), + // redirectOn4xx(this.router, this.authService), + // shareReplay(1) + // ); + + this.processRD$ = this.processService.notifyOnCompletion(this.route.snapshot.params.id).pipe( + redirectOn4xx(this.router, this.authService) ); this.filesRD$ = this.processRD$.pipe( @@ -127,52 +129,69 @@ export class ProcessDetailComponent implements OnInit, OnDestroy { ); } - refresh() { - this.processRD$ = this.processService.findById( - this.route.snapshot.params.id, - false, - true, - followLink('script') - ).pipe( - getFirstSucceededRemoteData(), - redirectOn4xx(this.router, this.authService), - tap((processRemoteData: RemoteData) => { - if (!this.isProcessFinished(processRemoteData.payload)) { - this.startRefreshTimer(); - } - }), - shareReplay(1) - ); + // refresh() { - this.filesRD$ = this.processRD$.pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((process: Process) => this.processService.getFiles(process.processId)) - ); - } + //////////////////////////////////////////////////////////////////////////////// - startRefreshTimer() { - this.refreshCounter$.next(0); + // this.processRD$ = this.processService.findById( + // this.route.snapshot.params.id, + // false, + // true, + // followLink('script') + // ).pipe( + // // First get the process state + // getFirstSucceededRemoteData(), - this.refreshTimerSub = interval(1000).subscribe( - value => { - if (value > 5) { - setTimeout(() => { - this.refresh(); - this.stopRefreshTimer(); - this.refreshCounter$.next(0); - }, 1); - } else { - this.refreshCounter$.next(5 - value); - } - }); - } + // // Error if it goes wrong + // redirectOn4xx(this.router, this.authService), - stopRefreshTimer() { - if (hasValue(this.refreshTimerSub)) { - this.refreshTimerSub.unsubscribe(); - this.refreshTimerSub = undefined; - } - } + // // If process is not finished, start the refresh timer + // tap((processRemoteData: RemoteData) => { + // if (!this.isProcessFinished(processRemoteData.payload)) { + // this.startRefreshTimer(); + // } + // }), + + // // ??? + // shareReplay(1) + // ); + // this.filesRD$ = this.processRD$.pipe( + // getFirstSucceededRemoteDataPayload(), + // switchMap((process: Process) => this.processService.getFiles(process.processId)) + // ); + // } + + // // TODO delete + // // call refresh after 5 sec + // startRefreshTimer() { + // this.refreshCounter$.next(0); + // + // // TODO delete comment + // // This fires every 1000 ms with an incrementing value. + // // So the first time this fires, it adds the value 5 to the refresh counter + // // the second time, it adds the value 4, + // // etc. + // // If the value exceeds 5, the refresh timer is stopped and this.refresh is called. + // this.refreshTimerSub = interval(1000).subscribe( + // value => { + // if (value > 5) { + // setTimeout(() => { + // this.refresh(); + // this.stopRefreshTimer(); + // this.refreshCounter$.next(0); + // }, 1); + // } else { + // this.refreshCounter$.next(5 - value); + // } + // }); + // } + // + // stopRefreshTimer() { + // if (hasValue(this.refreshTimerSub)) { + // this.refreshTimerSub.unsubscribe(); + // this.refreshTimerSub = undefined; + // } + // } /** * Get the name of a bitstream @@ -276,8 +295,4 @@ export class ProcessDetailComponent implements OnInit, OnDestroy { closeModal() { this.modalRef.close(); } - - ngOnDestroy(): void { - this.stopRefreshTimer(); - } } From 9b556fd7039449c9b8a762cb335f0b1b813001da Mon Sep 17 00:00:00 2001 From: Francesco Bacchelli Date: Tue, 22 Aug 2023 11:16:30 +0200 Subject: [PATCH 006/159] Merge branch CST-5249_suggestion of https://github.com/4Science/DSpace into CST-11299 --- .../builders/remote-data-build.service.ts | 1 + src/app/core/core.module.ts | 6 +- .../suggestion-objects.resource-type.ts | 16 ----- .../suggestion-source-object.resource-type.ts | 9 +++ .../models/suggestion-source.model.ts | 2 +- .../suggestion-target-object.resource-type.ts | 9 +++ .../models/suggestion-target.model.ts | 2 +- .../source/suggestion-source-data.service.ts | 2 +- .../suggestions-data.service.ts | 4 +- .../target/suggestion-target-data.service.ts | 3 +- src/app/home-page/home-page.component.html | 1 + src/app/home-page/home-page.module.ts | 18 ++--- src/app/menu.resolver.ts | 12 ++++ .../my-dspace-page.component.html | 1 + .../my-dspace-page/my-dspace-page.module.ts | 18 ++--- .../profile-page/profile-page.component.html | 1 + src/app/profile-page/profile-page.module.ts | 16 +++-- .../dso-selector-modal-wrapper.component.ts | 18 ++++- .../reciter-suggestions/selectors.ts | 10 +-- .../suggestion-targets.component.ts | 1 + .../suggestion-targets.effects.ts | 2 +- .../suggestion-targets.state.service.ts | 14 ++-- .../suggestions-popup.component.ts | 2 + .../suggestions.service.ts | 4 +- .../suggestion-notifications.module.ts | 12 ++++ src/assets/i18n/en.json5 | 68 +++++++++++++++++++ src/themes/custom/lazy-theme.module.ts | 3 + 27 files changed, 193 insertions(+), 62 deletions(-) create mode 100644 src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts create mode 100644 src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 075bf3ca0c..5b5e362406 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -161,6 +161,7 @@ export class RemoteDataBuildService { } else { // in case the elements of the paginated list were already filled in, because they're UnCacheableObjects paginatedList.page = paginatedList.page + .filter((obj: any) => obj != null) .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e176af7d55..7acf132df9 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -185,6 +185,8 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; +import { SuggestionTarget } from './suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; +import { SuggestionSource } from './suggestion-notifications/reciter-suggestions/models/suggestion-source.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -386,7 +388,9 @@ export const models = IdentifierData, Subscription, ItemRequest, - BulkAccessConditionOptions + BulkAccessConditionOptions, + SuggestionTarget, + SuggestionSource ]; @NgModule({ diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts index e31006959f..8f87027a8c 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type.ts @@ -1,21 +1,5 @@ import { ResourceType } from '../../../shared/resource-type'; -/** - * The resource type for the Suggestion Target object - * - * Needs to be in a separate file to prevent circular - * dependencies in webpack. - */ -export const SUGGESTION_TARGET = new ResourceType('suggestiontarget'); - -/** - * The resource type for the Suggestion Source object - * - * Needs to be in a separate file to prevent circular - * dependencies in webpack. - */ -export const SUGGESTION_SOURCE = new ResourceType('suggestionsource'); - /** * The resource type for the Suggestion object * diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts new file mode 100644 index 0000000000..2e26fe4301 --- /dev/null +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Suggestion Source object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_SOURCE = new ResourceType('suggestionsource'); diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts index 64ddf9863c..007520800d 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-source.model.ts @@ -1,6 +1,6 @@ import { autoserialize, deserialize } from 'cerialize'; -import { SUGGESTION_SOURCE } from './suggestion-objects.resource-type'; +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'; diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts new file mode 100644 index 0000000000..71dd41912a --- /dev/null +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for the Suggestion Target object + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUGGESTION_TARGET = new ResourceType('suggestiontarget'); diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts index 970a8e7e28..2afe170e77 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model.ts @@ -2,7 +2,7 @@ import { autoserialize, deserialize } from 'cerialize'; import { CacheableObject } from '../../../cache/cacheable-object.model'; -import { SUGGESTION_TARGET } from './suggestion-objects.resource-type'; +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'; diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service.ts b/src/app/core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service.ts index e4286c6b5b..c3e142044e 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { dataService } from '../../../data/base/data-service.decorator'; -import { SUGGESTION_SOURCE } from '../models/suggestion-objects.resource-type'; +import { SUGGESTION_SOURCE } from '../models/suggestion-source-object.resource-type'; import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { SuggestionSource } from '../models/suggestion-source.model'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts b/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts index abdcbca0c1..944a13e3a3 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/suggestions-data.service.ts @@ -168,8 +168,8 @@ export class SuggestionsDataService { ...linksToFollow: FollowLinkConfig[] ): Observable>> { options.searchParams = [new RequestParam('target', userId)]; - - return this.suggestionTargetsDataService.getTargetsByUser(this.searchFindByTargetMethod, options, ...linksToFollow); + //return this.suggestionTargetsDataService.getTargetsByUser(this.searchFindByTargetMethod, options, ...linksToFollow); + return this.suggestionTargetsDataService.getTargetsByUser(userId, options, ...linksToFollow); } /** diff --git a/src/app/core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service.ts b/src/app/core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service.ts index 372d1f9917..ce5f131c1d 100644 --- a/src/app/core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service.ts +++ b/src/app/core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { dataService } from '../../../data/base/data-service.decorator'; -import { SUGGESTION_TARGET } from '../models/suggestion-objects.resource-type'; + import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { SuggestionTarget } from '../models/suggestion-target.model'; import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data'; @@ -20,6 +20,7 @@ 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) diff --git a/src/app/home-page/home-page.component.html b/src/app/home-page/home-page.component.html index caa86ac290..49329b3f04 100644 --- a/src/app/home-page/home-page.component.html +++ b/src/app/home-page/home-page.component.html @@ -7,3 +7,4 @@ + diff --git a/src/app/home-page/home-page.module.ts b/src/app/home-page/home-page.module.ts index 1681abd805..00f9dbd8f9 100644 --- a/src/app/home-page/home-page.module.ts +++ b/src/app/home-page/home-page.module.ts @@ -13,6 +13,7 @@ import { RecentItemListComponent } from './recent-item-list/recent-item-list.com import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component'; +import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.module'; const DECLARATIONS = [ HomePageComponent, @@ -25,14 +26,15 @@ const DECLARATIONS = [ ]; @NgModule({ - imports: [ - CommonModule, - SharedModule.withEntryComponents(), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - HomePageRoutingModule, - StatisticsModule.forRoot() - ], + imports: [ + CommonModule, + SharedModule.withEntryComponents(), + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents(), + HomePageRoutingModule, + StatisticsModule.forRoot(), + SuggestionNotificationsModule + ], declarations: [ ...DECLARATIONS, ], diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 7a3a16d626..70e2b6462f 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -47,6 +47,7 @@ import { import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component'; +import { NOTIFICATIONS_RECITER_SUGGESTION_PATH } from './admin/admin-notifications/admin-notifications-routing-paths'; /** * Creates all of the app's menus @@ -555,6 +556,17 @@ export class MenuResolver implements Resolve { link: '/admin/notifications/quality-assurance' } as LinkMenuItemModel, }, + { + id: 'notifications_reciter', + parentID: 'notifications', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.notifications_reciter', + link: '/admin/notifications/' + NOTIFICATIONS_RECITER_SUGGESTION_PATH + } as LinkMenuItemModel, + }, /* Admin Search */ { id: 'admin_search', diff --git a/src/app/my-dspace-page/my-dspace-page.component.html b/src/app/my-dspace-page/my-dspace-page.component.html index ea5784170f..c5e49b0cec 100644 --- a/src/app/my-dspace-page/my-dspace-page.component.html +++ b/src/app/my-dspace-page/my-dspace-page.component.html @@ -1,5 +1,6 @@
+
+ diff --git a/src/app/profile-page/profile-page.module.ts b/src/app/profile-page/profile-page.module.ts index 0e2902de33..c5bfad1713 100644 --- a/src/app/profile-page/profile-page.module.ts +++ b/src/app/profile-page/profile-page.module.ts @@ -12,16 +12,18 @@ import { ThemedProfilePageComponent } from './themed-profile-page.component'; import { FormModule } from '../shared/form/form.module'; import { UiSwitchModule } from 'ngx-ui-switch'; import { ProfileClaimItemModalComponent } from './profile-claim-item-modal/profile-claim-item-modal.component'; +import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.module'; @NgModule({ - imports: [ - ProfilePageRoutingModule, - CommonModule, - SharedModule, - FormModule, - UiSwitchModule - ], + imports: [ + ProfilePageRoutingModule, + CommonModule, + SharedModule, + FormModule, + UiSwitchModule, + SuggestionNotificationsModule + ], exports: [ ProfilePageComponent, ThemedProfilePageComponent, diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index 3f81687c9f..6798449094 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -29,6 +29,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ @Input() dsoRD: RemoteData; + /** + * Representing if component should emit value of selected entries or navigate + */ + @Input() emitOnly = false; + /** * Optional header to display above the selection list * Supports i18n keys @@ -50,6 +55,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ action: SelectorActionType; + /** + * Event emitted when a DSO entry is selected if emitOnly is set to true + */ + @Output() select: EventEmitter = new EventEmitter(); + /** * Default DSO ordering */ @@ -93,7 +103,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ selectObject(dso: DSpaceObject) { this.close(); - this.navigate(dso); + if (this.emitOnly) { + this.select.emit(dso); + } else { + this.navigate(dso); + } } /** diff --git a/src/app/suggestion-notifications/reciter-suggestions/selectors.ts b/src/app/suggestion-notifications/reciter-suggestions/selectors.ts index cbb99fe869..ee90613271 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/selectors.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/selectors.ts @@ -27,7 +27,7 @@ export function reciterSuggestionTargetStateSelector(): MemoizedSelector { return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); @@ -47,7 +47,7 @@ export const isReciterSuggestionTargetLoadedSelector = createSelector(_getRecite * @function isDeduplicationSetsProcessingSelector * @return {boolean} */ -export const isreciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, +export const isReciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.processing ); @@ -56,7 +56,7 @@ export const isreciterSuggestionTargetProcessingSelector = createSelector(_getRe * @function getreciterSuggestionTargetTotalPagesSelector * @return {number} */ -export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, +export const getReciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.totalPages ); @@ -65,7 +65,7 @@ export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getR * @function getreciterSuggestionTargetCurrentPageSelector * @return {number} */ -export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, +export const getReciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.currentPage ); @@ -74,7 +74,7 @@ export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_get * @function getreciterSuggestionTargetTotalsSelector * @return {number} */ -export const getreciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, +export const getReciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, (state: SuggestionNotificationsState) => state.suggestionTarget.totalElements ); diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts index cfa168e2b8..e5524765da 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.component.ts @@ -136,6 +136,7 @@ export class SuggestionTargetsComponent implements OnInit { distinctUntilChanged(), take(1) ).subscribe((options: PaginationComponentOptions) => { + console.log('HELLO suggestion called!', options); this.suggestionTargetsStateService.dispatchRetrieveReciterSuggestionTargets( this.source, options.pageSize, diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index 3718e0cfad..6ab587a6c2 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -62,7 +62,7 @@ export class SuggestionTargetsEffects { /** * Fetch the current user suggestion */ - refreshUserTargets$ = createEffect(() => this.actions$.pipe( + RefreshUserSuggestionsAction = createEffect(() => this.actions$.pipe( ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS), switchMap((action: RefreshUserSuggestionsAction) => { return this.store$.select((state: any) => state.core.auth.userId) diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts index 97792df6af..fec9e01f3c 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts @@ -7,10 +7,10 @@ import { map } from 'rxjs/operators'; import { getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsVisitedSelector, - getreciterSuggestionTargetCurrentPageSelector, - getreciterSuggestionTargetTotalsSelector, + getReciterSuggestionTargetCurrentPageSelector, + getReciterSuggestionTargetTotalsSelector, isReciterSuggestionTargetLoadedSelector, - isreciterSuggestionTargetProcessingSelector, + isReciterSuggestionTargetProcessingSelector, reciterSuggestionTargetObjectSelector } from '../selectors'; import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model'; @@ -74,7 +74,7 @@ export class SuggestionTargetsStateService { * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ public isReciterSuggestionTargetsProcessing(): Observable { - return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector)); + return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector)); } /** @@ -84,7 +84,7 @@ export class SuggestionTargetsStateService { * The number of the Reciter Suggestion Targets pages. */ public getReciterSuggestionTargetsTotalPages(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector)); } /** @@ -94,7 +94,7 @@ export class SuggestionTargetsStateService { * The number of the current Reciter Suggestion Targets page. */ public getReciterSuggestionTargetsCurrentPage(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetCurrentPageSelector)); + return this.store.pipe(select(getReciterSuggestionTargetCurrentPageSelector)); } /** @@ -104,7 +104,7 @@ export class SuggestionTargetsStateService { * The number of the Reciter Suggestion Targets. */ public getReciterSuggestionTargetsTotals(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector)); } /** diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts index 0195f074ae..c0f94cadce 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions-popup/suggestions-popup.component.ts @@ -31,6 +31,8 @@ export class SuggestionsPopupComponent implements OnInit, OnDestroy { } public initializePopup() { + console.log('POPUP INIT dispatchRefreshUserSuggestionsAction'); + this.reciterSuggestionStateService.dispatchRefreshUserSuggestionsAction(); const notifier = new Subject(); this.subscription = combineLatest([ this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(), diff --git a/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts b/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts index 662b4c03de..b760ddf34d 100644 --- a/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts +++ b/src/app/suggestion-notifications/reciter-suggestions/suggestions.service.ts @@ -49,7 +49,9 @@ export class SuggestionsService { /** * Initialize the service variables. * @param {AuthService} authService - * @param {ResearcherProfileService} researcherProfileService + * @param {ResearcherProfileDataService} researcherProfileService + * @param {SuggestionSourceDataService} suggestionSourceDataService + * @param {SuggestionTargetDataService} suggestionTargetDataService * @param {SuggestionsDataService} suggestionsDataService */ constructor( diff --git a/src/app/suggestion-notifications/suggestion-notifications.module.ts b/src/app/suggestion-notifications/suggestion-notifications.module.ts index 4369d17375..fa9d81eb75 100644 --- a/src/app/suggestion-notifications/suggestion-notifications.module.ts +++ b/src/app/suggestion-notifications/suggestion-notifications.module.ts @@ -40,6 +40,15 @@ import { } from './reciter-suggestions/suggestions-notification/suggestions-notification.component'; import { SuggestionsService } from './reciter-suggestions/suggestions.service'; import { SuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service'; +import { + SuggestionSourceDataService +} from '../core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service'; +import { + SuggestionTargetDataService +} from '../core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service'; +import { + SuggestionTargetsStateService +} from './reciter-suggestions/suggestion-targets/suggestion-targets.state.service'; const MODULES = [ CommonModule, @@ -77,6 +86,9 @@ const PROVIDERS = [ QualityAssuranceSourceDataService, QualityAssuranceEventDataService, SuggestionsService, + SuggestionSourceDataService, + SuggestionTargetDataService, + SuggestionTargetsStateService, SuggestionsDataService ]; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4c13ec73d1..0d90ca693d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -24,6 +24,10 @@ "404.page-not-found": "page not found", + "admin.notifications.recitersuggestion.breadcrumbs": "Suggestions", + + "admin.notifications.recitersuggestion.page.title": "Suggestions", + "error-page.description.401": "unauthorized", "error-page.description.403": "forbidden", @@ -3052,6 +3056,12 @@ "mydspace.view-btn": "View", + "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.", + "nav.browse.header": "All of DSpace", "nav.community-browse.header": "By Community", @@ -3514,6 +3524,64 @@ "media-viewer.playlist": "Playlist", + "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", + "register-email.title": "New user registration", "register-page.create-profile.header": "Create Profile", diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index edb3f5478c..771e9e1b08 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -156,6 +156,8 @@ import { ItemStatusComponent } from './app/item-page/edit-item-page/item-status/ import { EditBitstreamPageComponent } from './app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component'; import { FormModule } from '../../app/shared/form/form.module'; import { RequestCopyModule } from 'src/app/request-copy/request-copy.module'; +import { SuggestionNotificationsModule } from '../../app/suggestion-notifications/suggestion-notifications.module'; + const DECLARATIONS = [ FileSectionComponent, @@ -299,6 +301,7 @@ const DECLARATIONS = [ NgxGalleryModule, FormModule, RequestCopyModule, + SuggestionNotificationsModule ], declarations: DECLARATIONS, exports: [ From c4d57770c74959dbd73ec5fb338692db279942eb Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 24 Aug 2023 10:00:21 +0200 Subject: [PATCH 007/159] ProcessDataService.notifyOnCompletion tests (WIP) --- .../processes/process-data.service.spec.ts | 48 +++++++++---------- .../data/processes/process-data.service.ts | 11 +++-- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index 6e7ce51502..bf42b6b9cf 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -18,7 +18,6 @@ import { Process } from '../../../process-page/processes/process.model'; import { ProcessStatus } from '../../../process-page/processes/process-status.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { CoreModule } from '../../core.module'; import { ReducerManager } from '@ngrx/store'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; @@ -27,7 +26,7 @@ import { NotificationsService } from '../../../shared/notifications/notification describe('ProcessDataService', () => { describe('composition', () => { - const initService = () => new ProcessDataService(null, null, null, null, null, null); + const initService = () => new ProcessDataService(null, null, null, null, null, null, null); testFindAllDataImplementation(initService); testDeleteDataImplementation(initService); }); @@ -38,13 +37,11 @@ describe('ProcessDataService', () => { describe('notifyOnCompletion', () => { beforeEach(waitForAsync(() => { - requestService = jasmine.createSpyObj('requestService', ['setStaleByHrefSubstring']); - TestBed.configureTestingModule({ imports: [], providers: [ ProcessDataService, - { provide: RequestService, useValue: requestService }, + { provide: RequestService, useValue: null }, { provide: RemoteDataBuildService, useValue: null }, { provide: ObjectCacheService, useValue: null }, { provide: ReducerManager, useValue: null }, @@ -56,6 +53,7 @@ describe('ProcessDataService', () => { }); processDataService = TestBed.inject(ProcessDataService); + spyOn(processDataService, 'invalidateByHref'); })); it('TODO', () => { @@ -71,33 +69,35 @@ describe('ProcessDataService', () => { let process$ = processDataService.notifyOnCompletion('instantly'); process$.subscribe((rd) => { expect(processDataService.findById).toHaveBeenCalledTimes(1); - expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled(); + expect(processDataService.invalidateByHref).not.toHaveBeenCalled(); }); expect(process$).toBeObservable(cold('(c|)', { 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) })); }); - // it('TODO2', () => { - // let completedProcess = new Process(); - // completedProcess.processStatus = ProcessStatus.COMPLETED; + it('TODO2', () => { + let runningProcess = new Process(); + runningProcess.processStatus = ProcessStatus.RUNNING; + let completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; - // spyOn(processDataService, 'findById').and.returnValue( - // cold('p 150ms (c|)', { - // 'p': new RemoteData(0, 0, 0, RequestEntryState., null, completedProcess), - // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - // }) - // ); + spyOn(processDataService, 'findById').and.returnValue( + cold('p 150ms (c|)', { + 'p': new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcess), + 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + }) + ); - // let process$ = processDataService.notifyOnCompletion('foo', 100); - // expect(process$).toBeObservable(cold('---(c|)', { - // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - // })); - // process$.subscribe((rd) => { - // expect(processDataService.findById).toHaveBeenCalledTimes(1); - // expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled(); - // }); - // }); + let process$ = processDataService.notifyOnCompletion('foo', 100); + // expect(process$).toBeObservable(cold('- 800ms (c|)', { + // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + // })); + process$.subscribe((rd) => { + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index f550407a59..f0c0829e85 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, NgZone } from '@angular/core'; import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; @@ -36,6 +36,7 @@ export class ProcessDataService extends IdentifiableDataService impleme protected halService: HALEndpointService, protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, + protected zone: NgZone, ) { super('processes', requestService, rdbService, objectCache, halService); @@ -129,9 +130,11 @@ export class ProcessDataService extends IdentifiableDataService impleme this.activelyBeingPolled.delete(processId); sub.unsubscribe(); } else { - setTimeout(() => { - this.requestService.setStaleByHrefSubstring(process._links.self.href); - }, pollingIntervalInMs); + this.zone.runOutsideAngular(() => + setTimeout(() => { + this.invalidateByHref(process._links.self.href); + }, pollingIntervalInMs) + ); } }); } From bd6648703c2c73ec552adf3bec13e7bf771fcfe6 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 25 Aug 2023 11:17:36 +0200 Subject: [PATCH 008/159] 104938 Add flush to process polling tests --- .../processes/process-data.service.spec.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index bf42b6b9cf..ccdb65c417 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -9,7 +9,7 @@ import { testFindAllDataImplementation } from '../base/find-all-data.spec'; import { ProcessDataService } from './process-data.service'; import { testDeleteDataImplementation } from '../base/delete-data.spec'; -import { cold } from 'jasmine-marbles'; +import { cold, getTestScheduler } from 'jasmine-marbles'; import { waitForAsync, TestBed } from '@angular/core/testing'; import { RequestService } from '../request.service'; import { RemoteData } from '../remote-data'; @@ -23,6 +23,7 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; import { BitstreamFormatDataService } from '../bitstream-format-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TestScheduler } from 'rxjs/testing'; describe('ProcessDataService', () => { describe('composition', () => { @@ -34,9 +35,11 @@ describe('ProcessDataService', () => { let requestService; let processDataService; let remoteDataBuildService; + let scheduler: TestScheduler; describe('notifyOnCompletion', () => { beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); TestBed.configureTestingModule({ imports: [], providers: [ @@ -90,13 +93,12 @@ describe('ProcessDataService', () => { ); let process$ = processDataService.notifyOnCompletion('foo', 100); - // expect(process$).toBeObservable(cold('- 800ms (c|)', { - // 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - // })); - process$.subscribe((rd) => { - expect(processDataService.findById).toHaveBeenCalledTimes(1); - expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); - }); + expect(process$).toBeObservable(cold('- 150ms (c|)', { + 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) + })); + scheduler.flush(); + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); }); }); From 3be90ebe460b9910ab2c86bbc15d27918a7c22a4 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 29 Aug 2023 18:39:39 +0200 Subject: [PATCH 009/159] rewrite notifyOnCompletion as autoRefreshUntilCompletion, fix ProcessDetailComponent, and the ProcessDataService tests --- ...llection-source-controls.component.spec.ts | 8 +- .../collection-source-controls.component.ts | 6 +- .../processes/process-data.service.spec.ts | 107 +++++++++------- .../data/processes/process-data.service.ts | 114 +++++++++++++----- .../detail/process-detail.component.html | 4 +- .../detail/process-detail.component.ts | 104 +++------------- .../processes/process-status.model.ts | 8 +- src/assets/i18n/en.json5 | 2 + 8 files changed, 183 insertions(+), 170 deletions(-) diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts index 37a5d8642d..f717943e8e 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.spec.ts @@ -88,7 +88,7 @@ describe('CollectionSourceControlsComponent', () => { invoke: createSuccessfulRemoteDataObject$(process), }); processDataService = jasmine.createSpyObj('processDataService', { - notifyOnCompletion: createSuccessfulRemoteDataObject$(process), + autoRefreshUntilCompletion: createSuccessfulRemoteDataObject$(process), }); bitstreamService = jasmine.createSpyObj('bitstreamService', { findByHref: createSuccessfulRemoteDataObject$(bitstream), @@ -137,7 +137,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)}, ], []); - expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); + expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId); expect(bitstreamService.findByHref).toHaveBeenCalledWith(process._links.output.href); expect(notificationsService.info).toHaveBeenCalledWith(jasmine.anything() as any, 'Script text'); }); @@ -151,7 +151,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-r', value: null}, {name: '-c', value: collection.uuid}, ], []); - expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); + expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId); expect(notificationsService.success).toHaveBeenCalled(); }); }); @@ -164,7 +164,7 @@ describe('CollectionSourceControlsComponent', () => { {name: '-o', value: null}, {name: '-c', value: collection.uuid}, ], []); - expect(processDataService.notifyOnCompletion).toHaveBeenCalledWith(process.processId); + expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId); expect(notificationsService.success).toHaveBeenCalled(); }); }); diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts index e8c3666da0..ea2cb3e2f4 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts @@ -95,7 +95,7 @@ export class CollectionSourceControlsComponent implements OnDestroy { }), // filter out responses that aren't successful since the pinging of the process only needs to happen when the invocation was successful. filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)), map((rd) => rd.payload) ).subscribe((process: Process) => { if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { @@ -166,7 +166,7 @@ export class CollectionSourceControlsComponent implements OnDestroy { } }), filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)), map((rd) => rd.payload) ).subscribe((process) => { if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { @@ -226,7 +226,7 @@ export class CollectionSourceControlsComponent implements OnDestroy { } }), filter((rd) => rd.hasSucceeded && hasValue(rd.payload)), - switchMap((rd) => this.processDataService.notifyOnCompletion(rd.payload.processId)), + switchMap((rd) => this.processDataService.autoRefreshUntilCompletion(rd.payload.processId)), map((rd) => rd.payload) ).subscribe((process) => { if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index ccdb65c417..f58752ee97 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -7,10 +7,9 @@ */ import { testFindAllDataImplementation } from '../base/find-all-data.spec'; -import { ProcessDataService } from './process-data.service'; +import { ProcessDataService, TIMER_FACTORY } from './process-data.service'; import { testDeleteDataImplementation } from '../base/delete-data.spec'; -import { cold, getTestScheduler } from 'jasmine-marbles'; -import { waitForAsync, TestBed } from '@angular/core/testing'; +import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { RequestService } from '../request.service'; import { RemoteData } from '../remote-data'; import { RequestEntryState } from '../request-entry-state.model'; @@ -23,11 +22,20 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; import { BitstreamFormatDataService } from '../bitstream-format-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TestScheduler } from 'rxjs/testing'; +import { TestScheduler, RunHelpers } from 'rxjs/testing'; +import { cold } from 'jasmine-marbles'; +import { of } from 'rxjs'; describe('ProcessDataService', () => { + let testScheduler; + + const mockTimer = (fn: () => {}, interval: number) => { + fn(); + return 555; + }; + describe('composition', () => { - const initService = () => new ProcessDataService(null, null, null, null, null, null, null); + const initService = () => new ProcessDataService(null, null, null, null, null, null, null, null); testFindAllDataImplementation(initService); testDeleteDataImplementation(initService); }); @@ -35,11 +43,12 @@ describe('ProcessDataService', () => { let requestService; let processDataService; let remoteDataBuildService; - let scheduler: TestScheduler; - describe('notifyOnCompletion', () => { + describe('autoRefreshUntilCompletion', () => { beforeEach(waitForAsync(() => { - scheduler = getTestScheduler(); + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); TestBed.configureTestingModule({ imports: [], providers: [ @@ -52,6 +61,7 @@ describe('ProcessDataService', () => { { provide: DSOChangeAnalyzer, useValue: null }, { provide: BitstreamFormatDataService, useValue: null }, { provide: NotificationsService, useValue: null }, + { provide: TIMER_FACTORY, useValue: mockTimer }, ] }); @@ -59,50 +69,63 @@ describe('ProcessDataService', () => { spyOn(processDataService, 'invalidateByHref'); })); - it('TODO', () => { - let completedProcess = new Process(); - completedProcess.processStatus = ProcessStatus.COMPLETED; + it('should not do any polling when the process is already completed', () => { + testScheduler.run(({ cold, expectObservable }) => { + let completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; - spyOn(processDataService, 'findById').and.returnValue( - cold('(c|)', { - 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - }) - ); + const completedProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess); - let process$ = processDataService.notifyOnCompletion('instantly'); - process$.subscribe((rd) => { - expect(processDataService.findById).toHaveBeenCalledTimes(1); - expect(processDataService.invalidateByHref).not.toHaveBeenCalled(); + spyOn(processDataService, 'findById').and.returnValue( + cold('c', { + 'c': completedProcessRD + }) + ); + + let process$ = processDataService.autoRefreshUntilCompletion('instantly'); + expectObservable(process$).toBe('c', { + c: completedProcessRD + }); }); - expect(process$).toBeObservable(cold('(c|)', { - 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - })); + + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(processDataService.invalidateByHref).not.toHaveBeenCalled(); }); - it('TODO2', () => { - let runningProcess = new Process(); - runningProcess.processStatus = ProcessStatus.RUNNING; - let completedProcess = new Process(); - completedProcess.processStatus = ProcessStatus.COMPLETED; + it('should poll until a process completes', () => { + testScheduler.run(({ cold, expectObservable }) => { + const runningProcess = Object.assign(new Process(), { + _links: { + self: { + href: 'https://rest.api/processes/123' + } + } + }); + runningProcess.processStatus = ProcessStatus.RUNNING; + const completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; + const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcess); + const completedProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess); - spyOn(processDataService, 'findById').and.returnValue( - cold('p 150ms (c|)', { - 'p': new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcess), - 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - }) - ); + spyOn(processDataService, 'findById').and.returnValue( + cold('r 150ms c', { + 'r': runningProcessRD, + 'c': completedProcessRD + }) + ); + + let process$ = processDataService.autoRefreshUntilCompletion('foo', 100); + expectObservable(process$).toBe('r 150ms c', { + 'r': runningProcessRD, + 'c': completedProcessRD + }); + }); - let process$ = processDataService.notifyOnCompletion('foo', 100); - expect(process$).toBeObservable(cold('- 150ms (c|)', { - 'c': new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess) - })); - scheduler.flush(); expect(processDataService.findById).toHaveBeenCalledTimes(1); expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); }); - }); -}); + }); // /** // * Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it @@ -131,4 +154,4 @@ describe('ProcessDataService', () => { // expect(out).toBe('TEST findAll'); // }); // }); -// } +}); diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index f0c0829e85..a639ef24ea 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NgZone } from '@angular/core'; +import { Injectable, NgZone, Inject, InjectionToken } from '@angular/core'; import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; @@ -6,7 +6,7 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { Process } from '../../../process-page/processes/process.model'; import { PROCESS } from '../../../process-page/processes/process.resource-type'; import { Observable } from 'rxjs'; -import { switchMap, filter, take } from 'rxjs/operators'; +import { switchMap, filter, distinctUntilChanged, find } from 'rxjs/operators'; import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; import { RemoteData } from '../remote-data'; @@ -21,13 +21,23 @@ import { NotificationsService } from '../../../shared/notifications/notification import { NoContent } from '../../shared/NoContent.model'; import { getAllCompletedRemoteData } from '../../shared/operators'; import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; +import { hasValue } from '../../../shared/empty.util'; + +/** + * Create an InjectionToken for the default JS setTimeout function, purely so we can mock it during + * testing. (fakeAsync isn't working for this case) + */ +export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout>('timer', { + providedIn: 'root', + factory: () => setTimeout +}); @Injectable() @dataService(PROCESS) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { private findAllData: FindAllData; private deleteData: DeleteData; - protected activelyBeingPolled: Set = new Set(); + protected activelyBeingPolled: Map = new Map(); constructor( protected requestService: RequestService, @@ -37,6 +47,7 @@ export class ProcessDataService extends IdentifiableDataService impleme protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, protected zone: NgZone, + @Inject(TIMER_FACTORY) protected timer: (callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout ) { super('processes', requestService, rdbService, objectCache, halService); @@ -106,42 +117,83 @@ export class ProcessDataService extends IdentifiableDataService impleme return this.deleteData.deleteByHref(href, copyVirtualMetadata); } - // TODO - public notifyOnCompletion(processId: string, pollingIntervalInMs = 5000): Observable> { - const process$ = this.findById(processId, false, true, followLink('script')) + /** + * Return true if the given process has the given status + * @protected + */ + protected statusIs(process: Process, status: ProcessStatus): boolean { + return hasValue(process) && process.processStatus === status; + } + + /** + * Return true if the given process has the status COMPLETED or FAILED + */ + public hasCompletedOrFailed(process: Process): boolean { + return this.statusIs(process, ProcessStatus.COMPLETED) || + this.statusIs(process, ProcessStatus.FAILED); + } + + /** + * Clear the timeout for the given process, if that timeout exists + * @protected + */ + protected clearCurrentTimeout(processId: string): void { + const timeout = this.activelyBeingPolled.get(processId); + if (hasValue(timeout)) { + clearTimeout(timeout); + } + }; + + /** + * Poll the process with the given ID, using the given interval, until that process either + * completes successfully or fails + * + * Return an Observable for the Process. Note that this will also emit while the + * process is still running. It will only emit again when the process (not the RemoteData!) changes + * status. That makes it more convenient to retrieve that process for a component: you can replace + * a findByID call with this method, rather than having to do a separate findById, and then call + * this method + * @param processId + * @param pollingIntervalInMs + */ + public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000): Observable> { + const process$ = this.findById(processId, true, true, followLink('script')) .pipe( getAllCompletedRemoteData(), ); - // TODO: this is horrible - const statusIs = (process: Process, status: ProcessStatus) => - process.processStatus === status; + // Create a subscription that marks the data as stale if the process hasn't been completed and + // the polling interval time has been exceeded. + const sub = process$.pipe( + filter((processRD: RemoteData) => + !this.hasCompletedOrFailed(processRD.payload) && + !this.activelyBeingPolled.has(processId) + ) + ).subscribe((processRD: RemoteData) => { + this.clearCurrentTimeout(processId); + const nextTimeout = this.timer(() => { + this.activelyBeingPolled.delete(processId); + this.invalidateByHref(processRD.payload._links.self.href); + }, pollingIntervalInMs); - // If we have to wait too long for the result, we should mark the result as stale. - // However, we should make sure this happens only once (in case there are multiple observers waiting - // for the result). - if (!this.activelyBeingPolled.has(processId)) { - this.activelyBeingPolled.add(processId); + this.activelyBeingPolled.set(processId, nextTimeout); + }); - // Create a subscription that marks the data as stale if the polling interval time has been exceeded. - const sub = process$.subscribe((rd) => { - const process = rd.payload; - if (statusIs(process, ProcessStatus.COMPLETED) || statusIs(process, ProcessStatus.FAILED)) { - this.activelyBeingPolled.delete(processId); - sub.unsubscribe(); - } else { - this.zone.runOutsideAngular(() => - setTimeout(() => { - this.invalidateByHref(process._links.self.href); - }, pollingIntervalInMs) - ); - } - }); - } + // When the process completes create a one off subscription (the `find` completes the + // observable) that unsubscribes the previous one, removes the processId from the list of + // processes being polled and clears any running timeouts + process$.pipe( + find((processRD: RemoteData) => this.hasCompletedOrFailed(processRD.payload)) + ).subscribe(() => { + this.clearCurrentTimeout(processId); + this.activelyBeingPolled.delete(processId); + sub.unsubscribe(); + }); return process$.pipe( - filter(rd => statusIs(rd.payload, ProcessStatus.COMPLETED) || statusIs(rd.payload, ProcessStatus.FAILED)), - take(1) + distinctUntilChanged((previous: RemoteData, current: RemoteData) => + previous.payload.processStatus === current.payload.processStatus + ) ); } } diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 5f905cbfff..c25a6ad50a 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -5,8 +5,8 @@ {{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }} -
- Refreshing in {{ seconds }}s +
+ {{ 'process.detail.refreshing' | translate }}
diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index 18992eae2f..b1d3241422 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,8 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { finalize, map, switchMap, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription, interval } from 'rxjs'; +import { finalize, map, switchMap, take, tap, filter, find, startWith } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; @@ -14,7 +14,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload + getFirstSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { AlertType } from '../../shared/alert/aletr-type'; @@ -26,6 +26,7 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { getProcessListRoute } from '../process-page-routing.paths'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'ds-process-detail', @@ -76,7 +77,7 @@ export class ProcessDetailComponent implements OnInit { */ dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ'; - refreshCounter$ = new BehaviorSubject(0); + isRefreshing$: Observable; /** * Reference to NgbModal @@ -105,94 +106,29 @@ export class ProcessDetailComponent implements OnInit { * Display a 404 if the process doesn't exist */ ngOnInit(): void { - // this.processRD$ = this.route.data.pipe( - // map((data) => { - // if (isPlatformBrowser(this.platformId)) { - // if (!this.isProcessFinished(data.process.payload)) { - // this.startRefreshTimer(); - // } - // } + this.processRD$ = this.route.data.pipe( + switchMap((data) => { + if (isPlatformBrowser(this.platformId)) { + return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); + } else { + return [data.process as RemoteData]; + } + }), + redirectOn4xx(this.router, this.authService), + ); - // return data.process as RemoteData; - // }), - // redirectOn4xx(this.router, this.authService), - // shareReplay(1) - // ); - - this.processRD$ = this.processService.notifyOnCompletion(this.route.snapshot.params.id).pipe( - redirectOn4xx(this.router, this.authService) + this.isRefreshing$ = this.processRD$.pipe( + find((processRD: RemoteData) => this.processService.hasCompletedOrFailed(processRD.payload)), + map(() => false), + startWith(true) ); this.filesRD$ = this.processRD$.pipe( - getFirstSucceededRemoteDataPayload(), + getAllSucceededRemoteDataPayload(), switchMap((process: Process) => this.processService.getFiles(process.processId)) ); } - // refresh() { - - //////////////////////////////////////////////////////////////////////////////// - - // this.processRD$ = this.processService.findById( - // this.route.snapshot.params.id, - // false, - // true, - // followLink('script') - // ).pipe( - // // First get the process state - // getFirstSucceededRemoteData(), - - // // Error if it goes wrong - // redirectOn4xx(this.router, this.authService), - - // // If process is not finished, start the refresh timer - // tap((processRemoteData: RemoteData) => { - // if (!this.isProcessFinished(processRemoteData.payload)) { - // this.startRefreshTimer(); - // } - // }), - - // // ??? - // shareReplay(1) - // ); - // this.filesRD$ = this.processRD$.pipe( - // getFirstSucceededRemoteDataPayload(), - // switchMap((process: Process) => this.processService.getFiles(process.processId)) - // ); - // } - - // // TODO delete - // // call refresh after 5 sec - // startRefreshTimer() { - // this.refreshCounter$.next(0); - // - // // TODO delete comment - // // This fires every 1000 ms with an incrementing value. - // // So the first time this fires, it adds the value 5 to the refresh counter - // // the second time, it adds the value 4, - // // etc. - // // If the value exceeds 5, the refresh timer is stopped and this.refresh is called. - // this.refreshTimerSub = interval(1000).subscribe( - // value => { - // if (value > 5) { - // setTimeout(() => { - // this.refresh(); - // this.stopRefreshTimer(); - // this.refreshCounter$.next(0); - // }, 1); - // } else { - // this.refreshCounter$.next(5 - value); - // } - // }); - // } - // - // stopRefreshTimer() { - // if (hasValue(this.refreshTimerSub)) { - // this.refreshTimerSub.unsubscribe(); - // this.refreshTimerSub = undefined; - // } - // } - /** * Get the name of a bitstream * @param bitstream diff --git a/src/app/process-page/processes/process-status.model.ts b/src/app/process-page/processes/process-status.model.ts index b43340bffb..1ff42789d8 100644 --- a/src/app/process-page/processes/process-status.model.ts +++ b/src/app/process-page/processes/process-status.model.ts @@ -2,8 +2,8 @@ * List of process statuses */ export enum ProcessStatus { - SCHEDULED, - RUNNING, - COMPLETED, - FAILED + SCHEDULED = 'SCHEDULED', + RUNNING = 'RUNNING', + COMPLETED = 'COMPLETED', + FAILED = 'FAILED' } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6c91bae4c1..aa327c7934 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3216,6 +3216,8 @@ "process.detail.delete.error": "Something went wrong when deleting the process", + "process.detail.refreshing": "Auto-refreshing…", + "process.overview.table.finish": "Finish time (UTC)", "process.overview.table.id": "Process ID", From a59776d5a00b50497e987b372c91b28b17bf5152 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 1 Sep 2023 15:10:39 +0200 Subject: [PATCH 010/159] Failed attempt at fixing process-detail.component.spec.ts tests --- .../processes/process-data.service.spec.ts | 29 ------------------- .../detail/process-detail.component.spec.ts | 9 ++++-- .../detail/process-detail.component.ts | 6 +++- 3 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index f58752ee97..fe632096f5 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -124,34 +124,5 @@ describe('ProcessDataService', () => { expect(processDataService.findById).toHaveBeenCalledTimes(1); expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); }); - }); - -// /** -// * Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it -// */ -// export function testFindAllDataImplementation(serviceFactory: () => FindAllData) { -// let service; -// -// describe('FindAllData implementation', () => { -// const OPTIONS = Object.assign(new FindListOptions(), { elementsPerPage: 10, currentPage: 3 }); -// const FOLLOWLINKS = [ -// followLink('test'), -// followLink('something'), -// ]; -// -// beforeAll(() => { -// service = serviceFactory(); -// (service as any).findAllData = jasmine.createSpyObj('findAllData', { -// findAll: 'TEST findAll', -// }); -// }); -// -// it('should handle calls to findAll', () => { -// const out: any = service.findAll(OPTIONS, false, true, ...FOLLOWLINKS); -// -// expect((service as any).findAllData.findAll).toHaveBeenCalledWith(OPTIONS, false, true, ...FOLLOWLINKS); -// expect(out).toBe('TEST findAll'); -// }); -// }); }); diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index 68b97d0e5c..ba99342191 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -106,10 +106,12 @@ describe('ProcessDetailComponent', () => { content: { href: 'log-selflink' } } }); + const processRD$ = createSuccessfulRemoteDataObject$(process); processService = jasmine.createSpyObj('processService', { getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)), delete: createSuccessfulRemoteDataObject$(null), - findById: createSuccessfulRemoteDataObject$(process), + findById: processRD$, + autoRefreshUntilCompletion: processRD$ }); bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { findByHref: createSuccessfulRemoteDataObject$(logBitstream) @@ -132,7 +134,7 @@ describe('ProcessDetailComponent', () => { }); route = jasmine.createSpyObj('route', { - data: observableOf({ process: createSuccessfulRemoteDataObject(process) }), + data: observableOf({ process: processRD$ }), snapshot: { params: { id: process.processId } } @@ -158,7 +160,8 @@ describe('ProcessDetailComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: Router, useValue: router }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + // schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [] }).compileComponents(); })); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index b1d3241422..d96c47b371 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -109,7 +109,10 @@ export class ProcessDetailComponent implements OnInit { this.processRD$ = this.route.data.pipe( switchMap((data) => { if (isPlatformBrowser(this.platformId)) { - return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); + const x = this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); + //[data.process as RemoteData]; + console.log("ASDF", x); + return x; } else { return [data.process as RemoteData]; } @@ -117,6 +120,7 @@ export class ProcessDetailComponent implements OnInit { redirectOn4xx(this.router, this.authService), ); + this.processRD$.subscribe(x => console.log("QWER", x)); this.isRefreshing$ = this.processRD$.pipe( find((processRD: RemoteData) => this.processService.hasCompletedOrFailed(processRD.payload)), map(() => false), From 53b0af100d5dcbd07d5ac2f8c8744da7ed7240be Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Fri, 1 Sep 2023 16:11:32 +0200 Subject: [PATCH 011/159] 104938 Fix ProcessDetailComponent tests --- .../collection-source-controls.component.ts | 3 +- .../processes/process-data.service.spec.ts | 6 +-- .../data/processes/process-data.service.ts | 38 +++++++++---------- .../detail/process-detail.component.spec.ts | 18 +++++---- .../detail/process-detail.component.ts | 12 ++---- 5 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts index ea2cb3e2f4..58f41acf34 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts @@ -3,13 +3,12 @@ import { ScriptDataService } from '../../../../core/data/processes/script-data.s import { ContentSource } from '../../../../core/shared/content-source.model'; import { ProcessDataService } from '../../../../core/data/processes/process-data.service'; import { - getAllCompletedRemoteData, getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { filter, map, switchMap, tap } from 'rxjs/operators'; -import { hasValue, hasValueOperator } from '../../../../shared/empty.util'; +import { hasValue } from '../../../../shared/empty.util'; import { ProcessStatus } from '../../../../process-page/processes/process-status.model'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { RequestService } from '../../../../core/data/request.service'; diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index fe632096f5..d66560b083 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -9,7 +9,7 @@ import { testFindAllDataImplementation } from '../base/find-all-data.spec'; import { ProcessDataService, TIMER_FACTORY } from './process-data.service'; import { testDeleteDataImplementation } from '../base/delete-data.spec'; -import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { waitForAsync, TestBed } from '@angular/core/testing'; import { RequestService } from '../request.service'; import { RemoteData } from '../remote-data'; import { RequestEntryState } from '../request-entry-state.model'; @@ -22,9 +22,7 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; import { BitstreamFormatDataService } from '../bitstream-format-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TestScheduler, RunHelpers } from 'rxjs/testing'; -import { cold } from 'jasmine-marbles'; -import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; describe('ProcessDataService', () => { let testScheduler; diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index a639ef24ea..e17b0b1f19 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -35,6 +35,22 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v @Injectable() @dataService(PROCESS) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { + /** + * Return true if the given process has the given status + * @protected + */ + protected static statusIs(process: Process, status: ProcessStatus): boolean { + return hasValue(process) && process.processStatus === status; + } + + /** + * Return true if the given process has the status COMPLETED or FAILED + */ + public static hasCompletedOrFailed(process: Process): boolean { + return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) || + ProcessDataService.statusIs(process, ProcessStatus.FAILED); + } + private findAllData: FindAllData; private deleteData: DeleteData; protected activelyBeingPolled: Map = new Map(); @@ -117,22 +133,6 @@ export class ProcessDataService extends IdentifiableDataService impleme return this.deleteData.deleteByHref(href, copyVirtualMetadata); } - /** - * Return true if the given process has the given status - * @protected - */ - protected statusIs(process: Process, status: ProcessStatus): boolean { - return hasValue(process) && process.processStatus === status; - } - - /** - * Return true if the given process has the status COMPLETED or FAILED - */ - public hasCompletedOrFailed(process: Process): boolean { - return this.statusIs(process, ProcessStatus.COMPLETED) || - this.statusIs(process, ProcessStatus.FAILED); - } - /** * Clear the timeout for the given process, if that timeout exists * @protected @@ -142,7 +142,7 @@ export class ProcessDataService extends IdentifiableDataService impleme if (hasValue(timeout)) { clearTimeout(timeout); } - }; + } /** * Poll the process with the given ID, using the given interval, until that process either @@ -166,7 +166,7 @@ export class ProcessDataService extends IdentifiableDataService impleme // the polling interval time has been exceeded. const sub = process$.pipe( filter((processRD: RemoteData) => - !this.hasCompletedOrFailed(processRD.payload) && + !ProcessDataService.hasCompletedOrFailed(processRD.payload) && !this.activelyBeingPolled.has(processId) ) ).subscribe((processRD: RemoteData) => { @@ -183,7 +183,7 @@ export class ProcessDataService extends IdentifiableDataService impleme // observable) that unsubscribes the previous one, removes the processId from the list of // processes being polled and clears any running timeouts process$.pipe( - find((processRD: RemoteData) => this.hasCompletedOrFailed(processRD.payload)) + find((processRD: RemoteData) => ProcessDataService.hasCompletedOrFailed(processRD.payload)) ).subscribe(() => { this.clearCurrentTimeout(processId); this.activelyBeingPolled.delete(processId); diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index ba99342191..1af1edca99 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -35,7 +35,6 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { getProcessListRoute } from '../process-page-routing.paths'; -import {ProcessStatus} from '../processes/process-status.model'; describe('ProcessDetailComponent', () => { let component: ProcessDetailComponent; @@ -106,12 +105,11 @@ describe('ProcessDetailComponent', () => { content: { href: 'log-selflink' } } }); - const processRD$ = createSuccessfulRemoteDataObject$(process); processService = jasmine.createSpyObj('processService', { getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)), delete: createSuccessfulRemoteDataObject$(null), - findById: processRD$, - autoRefreshUntilCompletion: processRD$ + findById: createSuccessfulRemoteDataObject$(process), + autoRefreshUntilCompletion: createSuccessfulRemoteDataObject$(process) }); bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { findByHref: createSuccessfulRemoteDataObject$(logBitstream) @@ -134,7 +132,7 @@ describe('ProcessDetailComponent', () => { }); route = jasmine.createSpyObj('route', { - data: observableOf({ process: processRD$ }), + data: observableOf({ process: createSuccessfulRemoteDataObject$(process) }), snapshot: { params: { id: process.processId } } @@ -149,7 +147,12 @@ describe('ProcessDetailComponent', () => { providers: [ { provide: ActivatedRoute, - useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } + useValue: { + data: observableOf({ process: createSuccessfulRemoteDataObject(process) }), + snapshot: { + params: { id: process.processId } + } + } }, { provide: ProcessDataService, useValue: processService }, { provide: BitstreamDataService, useValue: bitstreamDataService }, @@ -160,8 +163,7 @@ describe('ProcessDetailComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: Router, useValue: router }, ], - // schemas: [CUSTOM_ELEMENTS_SCHEMA] - schemas: [] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index d96c47b371..c8e4507fd2 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,8 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, Observable, Subscription, interval } from 'rxjs'; -import { finalize, map, switchMap, take, tap, filter, find, startWith } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { finalize, map, switchMap, take, tap, find, startWith } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; @@ -109,10 +109,7 @@ export class ProcessDetailComponent implements OnInit { this.processRD$ = this.route.data.pipe( switchMap((data) => { if (isPlatformBrowser(this.platformId)) { - const x = this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); - //[data.process as RemoteData]; - console.log("ASDF", x); - return x; + return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); } else { return [data.process as RemoteData]; } @@ -120,9 +117,8 @@ export class ProcessDetailComponent implements OnInit { redirectOn4xx(this.router, this.authService), ); - this.processRD$.subscribe(x => console.log("QWER", x)); this.isRefreshing$ = this.processRD$.pipe( - find((processRD: RemoteData) => this.processService.hasCompletedOrFailed(processRD.payload)), + find((processRD: RemoteData) => ProcessDataService.hasCompletedOrFailed(processRD.payload)), map(() => false), startWith(true) ); From 9b5001e1d987bde1dd57248c35a69ae60bb8ea48 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 7 Sep 2023 10:19:33 +0200 Subject: [PATCH 012/159] 104938 Cleanup --- .../collection-source-controls.component.ts | 81 ----------------- .../detail/process-detail.component.spec.ts | 89 ------------------- 2 files changed, 170 deletions(-) diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts index 58f41acf34..185a1f938e 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts @@ -113,36 +113,6 @@ export class CollectionSourceControlsComponent implements OnDestroy { this.testConfigRunning$.next(false); } })); - - // getAllCompletedRemoteData(), - // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - // map((rd) => rd.payload), - // hasValueOperator(), - // ).subscribe((process: Process) => { - // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // // Ping the current process state every 5s - // setTimeout(() => { - // this.requestService.setStaleByHrefSubstring(process._links.self.href); - // }, 5000); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - // this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed')); - // this.testConfigRunning$.next(false); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - // this.bitstreamService.findByHref(process._links.output.href).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => { - // this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => { - // const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1') - // .replaceAll('The script has started', '') - // .replaceAll('The script has completed', ''); - // this.notificationsService.info(this.translateService.get('collection.source.controls.test.completed'), output); - // }); - // }); - // this.testConfigRunning$.next(false); - // } - // } - // )); } /** @@ -178,31 +148,6 @@ export class CollectionSourceControlsComponent implements OnDestroy { this.importRunning$.next(false); } })); - - // getAllCompletedRemoteData(), - // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - // map((rd) => rd.payload), - // hasValueOperator(), - // ).subscribe((process) => { - // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // // Ping the current process state every 5s - // setTimeout(() => { - // this.requestService.setStaleByHrefSubstring(process._links.self.href); - // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - // }, 5000); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - // this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed')); - // this.importRunning$.next(false); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - // this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed')); - // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - // this.importRunning$.next(false); - // } - // } - // )); } /** @@ -238,32 +183,6 @@ export class CollectionSourceControlsComponent implements OnDestroy { this.reImportRunning$.next(false); } })); - - // switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)), - // getAllCompletedRemoteData(), - // filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)), - // map((rd) => rd.payload), - // hasValueOperator(), - // ).subscribe((process) => { - // if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() && - // process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) { - // // Ping the current process state every 5s - // setTimeout(() => { - // this.requestService.setStaleByHrefSubstring(process._links.self.href); - // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - // }, 5000); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) { - // this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed')); - // this.reImportRunning$.next(false); - // } - // if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) { - // this.notificationsService.success(this.translateService.get('collection.source.controls.reset.completed')); - // this.requestService.setStaleByHrefSubstring(this.collection._links.self.href); - // this.reImportRunning$.next(false); - // } - // } - // )); } ngOnDestroy(): void { diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index 1af1edca99..241af9fd10 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -277,93 +277,4 @@ describe('ProcessDetailComponent', () => { expect(router.navigateByUrl).not.toHaveBeenCalled(); }); }); - - // describe('refresh counter', () => { - // const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter')); - - // describe('if process is completed', () => { - // beforeEach(() => { - // process.processStatus = ProcessStatus.COMPLETED; - // route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); - // }); - - // it('should not show', () => { - // spyOn(component, 'startRefreshTimer'); - - // const refreshCounter = queryRefreshCounter(); - // expect(refreshCounter).toBeNull(); - - // expect(component.startRefreshTimer).not.toHaveBeenCalled(); - // }); - // }); - - // describe('if process is not finished', () => { - // beforeEach(() => { - // process.processStatus = ProcessStatus.RUNNING; - // route.data = observableOf({process: createSuccessfulRemoteDataObject(process)}); - // fixture.detectChanges(); - // component.stopRefreshTimer(); - // }); - - // it('should call startRefreshTimer', () => { - // spyOn(component, 'startRefreshTimer'); - - // component.ngOnInit(); - // fixture.detectChanges(); // subscribe to process observable with async pipe - - // expect(component.startRefreshTimer).toHaveBeenCalled(); - // }); - - // it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => { - // spyOn(component, 'refresh'); - // spyOn(component, 'stopRefreshTimer'); - - // process.processStatus = ProcessStatus.COMPLETED; - // // set findbyId to return a completed process - // (processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process))); - - // component.ngOnInit(); - // fixture.detectChanges(); // subscribe to process observable with async pipe - - // expect(component.refresh).not.toHaveBeenCalled(); - - // expect(component.refreshCounter$.value).toBe(0); - - // tick(1001); // 1 second + 1 ms by the setTimeout - // expect(component.refreshCounter$.value).toBe(5); // 5 - 0 - - // tick(2001); // 2 seconds + 1 ms by the setTimeout - // expect(component.refreshCounter$.value).toBe(3); // 5 - 2 - - // tick(2001); // 2 seconds + 1 ms by the setTimeout - // expect(component.refreshCounter$.value).toBe(1); // 3 - 2 - - // tick(1001); // 1 second + 1 ms by the setTimeout - // expect(component.refreshCounter$.value).toBe(0); // 1 - 1 - - // tick(1000); // 1 second - - // expect(component.refresh).toHaveBeenCalledTimes(1); - // expect(component.stopRefreshTimer).toHaveBeenCalled(); - - // expect(component.refreshCounter$.value).toBe(0); - - // tick(1001); // 1 second + 1 ms by the setTimeout - // // startRefreshTimer not called again - // expect(component.refreshCounter$.value).toBe(0); - - // discardPeriodicTasks(); // discard any periodic tasks that have not yet executed - // })); - - // it('should show if refreshCounter is different from 0', () => { - // component.refreshCounter$.next(1); - // fixture.detectChanges(); - - // const refreshCounter = queryRefreshCounter(); - // expect(refreshCounter).not.toBeNull(); - // }); - - // }); - - // }); }); From 11e98f7e20894e4badb6d2ed3a78d155f56e6a2c Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Thu, 7 Sep 2023 11:36:52 +0200 Subject: [PATCH 013/159] Fix lint issue. --- src/app/core/data/processes/process-data.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index e17b0b1f19..3af755561c 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -35,6 +35,10 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v @Injectable() @dataService(PROCESS) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { + private findAllData: FindAllData; + private deleteData: DeleteData; + protected activelyBeingPolled: Map = new Map(); + /** * Return true if the given process has the given status * @protected @@ -51,10 +55,6 @@ export class ProcessDataService extends IdentifiableDataService impleme ProcessDataService.statusIs(process, ProcessStatus.FAILED); } - private findAllData: FindAllData; - private deleteData: DeleteData; - protected activelyBeingPolled: Map = new Map(); - constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, From a55eb97a6ac5bd4ee4a32d07b09369d5d2fa286a Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Fri, 20 Oct 2023 16:04:40 +0200 Subject: [PATCH 014/159] [CST-12043] fix: normalization for boolean --- src/app/shared/form/builder/form-builder.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index cf6f38bf7b..8bd449a07d 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -196,6 +196,7 @@ export class FormBuilderService extends DynamicFormService { return new FormFieldMetadataValueObject((controlValue as any).value, controlLanguage, authority, (controlValue as any).display, place, (controlValue as any).confidence); } } + return controlValue; }; const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => { From ffdeca69f45ff4b0ce811f237a314fc672816777 Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Fri, 20 Oct 2023 16:07:31 +0200 Subject: [PATCH 015/159] [CST-12043] feature: add primary bitstream switch --- .../workspaceitem-section-upload.model.ts | 5 +- .../custom-switch.component.html | 2 +- .../objects/submission-objects.actions.ts | 25 +++++ .../objects/submission-objects.reducer.ts | 47 ++++++++- .../section-upload-file-edit.component.html | 6 +- .../section-upload-file-edit.component.ts | 69 +++++++------ .../edit/section-upload-file-edit.model.ts | 13 +++ .../file/section-upload-file.component.html | 9 +- .../file/section-upload-file.component.ts | 97 ++++++++++++++++++- .../themed-section-upload-file.component.ts | 8 ++ .../upload/section-upload.component.html | 28 +++--- .../upload/section-upload.component.ts | 35 +++---- .../sections/upload/section-upload.service.ts | 36 ++++++- 13 files changed, 303 insertions(+), 77 deletions(-) diff --git a/src/app/core/submission/models/workspaceitem-section-upload.model.ts b/src/app/core/submission/models/workspaceitem-section-upload.model.ts index f98e0584eb..d992567df4 100644 --- a/src/app/core/submission/models/workspaceitem-section-upload.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-upload.model.ts @@ -4,7 +4,10 @@ import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-up * An interface to represent submission's upload section data. */ export interface WorkspaceitemSectionUploadObject { - + /** + * Primary bitstream flag + */ + primary: string | null; /** * A list of [[WorkspaceitemSectionUploadFileObject]] */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html index ed117a5021..572c99ad3a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html @@ -13,7 +13,7 @@ (blur)="onBlur($event)" (change)="onChange($event)" (focus)="onFocus($event)"/> -