mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'main' into DURACOM-195
# Conflicts: # src/assets/i18n/en.json5 # src/styles/_custom_variables.scss # src/themes/dspace/styles/_theme_css_variable_overrides.scss
This commit is contained in:
@@ -136,7 +136,7 @@ submission:
|
|||||||
# NOTE: example of configuration
|
# NOTE: example of configuration
|
||||||
# # NOTE: metadata name
|
# # NOTE: metadata name
|
||||||
# - name: dc.author
|
# - name: dc.author
|
||||||
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
||||||
# style: fas fa-user
|
# style: fas fa-user
|
||||||
- name: dc.author
|
- name: dc.author
|
||||||
style: fas fa-user
|
style: fas fa-user
|
||||||
@@ -147,18 +147,40 @@ submission:
|
|||||||
confidence:
|
confidence:
|
||||||
# NOTE: example of configuration
|
# NOTE: example of configuration
|
||||||
# # NOTE: confidence value
|
# # NOTE: confidence value
|
||||||
# - name: dc.author
|
# - value: 600
|
||||||
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
||||||
# style: fa-user
|
# style: text-success
|
||||||
|
# icon: fa-circle-check
|
||||||
|
# # NOTE: the class configured in property style is used by default, the icon property could be used in component
|
||||||
|
# configured to use a 'icon mode' display (mainly in edit-item page)
|
||||||
- value: 600
|
- value: 600
|
||||||
style: text-success
|
style: text-success
|
||||||
|
icon: fa-circle-check
|
||||||
- value: 500
|
- value: 500
|
||||||
style: text-info
|
style: text-info
|
||||||
|
icon: fa-gear
|
||||||
- value: 400
|
- value: 400
|
||||||
style: text-warning
|
style: text-warning
|
||||||
|
icon: fa-circle-question
|
||||||
|
- value: 300
|
||||||
|
style: text-muted
|
||||||
|
icon: fa-thumbs-down
|
||||||
|
- value: 200
|
||||||
|
style: text-muted
|
||||||
|
icon: fa-circle-exclamation
|
||||||
|
- value: 100
|
||||||
|
style: text-muted
|
||||||
|
icon: fa-circle-stop
|
||||||
|
- value: 0
|
||||||
|
style: text-muted
|
||||||
|
icon: fa-ban
|
||||||
|
- value: -1
|
||||||
|
style: text-muted
|
||||||
|
icon: fa-circle-xmark
|
||||||
# default configuration
|
# default configuration
|
||||||
- value: default
|
- value: default
|
||||||
style: text-muted
|
style: text-muted
|
||||||
|
icon: fa-circle-xmark
|
||||||
|
|
||||||
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
||||||
defaultLanguage: en
|
defaultLanguage: en
|
||||||
@@ -272,6 +294,8 @@ homePage:
|
|||||||
# No. of communities to list per page on the home page
|
# No. of communities to list per page on the home page
|
||||||
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
||||||
pageSize: 5
|
pageSize: 5
|
||||||
|
# Enable or disable the Discover filters on the homepage
|
||||||
|
showDiscoverFilters: false
|
||||||
|
|
||||||
# Item Config
|
# Item Config
|
||||||
item:
|
item:
|
||||||
@@ -285,8 +309,17 @@ item:
|
|||||||
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
||||||
pageSize: 5
|
pageSize: 5
|
||||||
|
|
||||||
|
# Community Page Config
|
||||||
|
community:
|
||||||
|
# Search tab config
|
||||||
|
searchSection:
|
||||||
|
showSidebar: true
|
||||||
|
|
||||||
# Collection Page Config
|
# Collection Page Config
|
||||||
collection:
|
collection:
|
||||||
|
# Search tab config
|
||||||
|
searchSection:
|
||||||
|
showSidebar: true
|
||||||
edit:
|
edit:
|
||||||
undoTimeout: 10000 # 10 seconds
|
undoTimeout: 10000 # 10 seconds
|
||||||
|
|
||||||
@@ -386,3 +419,8 @@ vocabularies:
|
|||||||
comcolSelectionSort:
|
comcolSelectionSort:
|
||||||
sortField: 'dc.title'
|
sortField: 'dc.title'
|
||||||
sortDirection: 'ASC'
|
sortDirection: 'ASC'
|
||||||
|
|
||||||
|
# Example of fallback collection for suggestions import
|
||||||
|
# suggestion:
|
||||||
|
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
|
||||||
|
# source: "openaire"
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the route parameters.
|
||||||
|
*/
|
||||||
|
export interface AdminNotificationsPublicationClaimPageParams {
|
||||||
|
pageId?: string;
|
||||||
|
pageSize?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that retrieve the route data before the route is activated.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AdminNotificationsPublicationClaimPageResolver implements Resolve<AdminNotificationsPublicationClaimPageParams> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving the parameters in the current route.
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsPublicationClaimPageParams {
|
||||||
|
return {
|
||||||
|
pageId: route.queryParams.pageId,
|
||||||
|
pageSize: parseInt(route.queryParams.pageSize, 10),
|
||||||
|
currentPage: parseInt(route.queryParams.page, 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('AdminNotificationsPublicationClaimPageComponent', () => {
|
||||||
|
let component: AdminNotificationsPublicationClaimPageComponent;
|
||||||
|
let fixture: ComponentFixture<AdminNotificationsPublicationClaimPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AdminNotificationsPublicationClaimPageComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AdminNotificationsPublicationClaimPageComponent
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-notifications-publication-claim-page',
|
||||||
|
templateUrl: './admin-notifications-publication-claim-page.component.html',
|
||||||
|
styleUrls: ['./admin-notifications-publication-claim-page.component.scss']
|
||||||
|
})
|
||||||
|
export class AdminNotificationsPublicationClaimPageComponent {
|
||||||
|
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
|||||||
import { getNotificationsModuleRoute } from '../admin-routing-paths';
|
import { getNotificationsModuleRoute } from '../admin-routing-paths';
|
||||||
|
|
||||||
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
|
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
|
||||||
|
export const PUBLICATION_CLAIMS_PATH = 'publication-claim';
|
||||||
|
|
||||||
export function getQualityAssuranceRoute(id: string) {
|
export function getQualityAssuranceRoute(id: string) {
|
||||||
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();
|
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();
|
||||||
|
@@ -4,6 +4,9 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
|
||||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths';
|
||||||
|
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
|
||||||
|
import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service';
|
||||||
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
|
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
|
||||||
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
||||||
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
||||||
@@ -20,6 +23,21 @@ import {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
canActivate: [ AuthenticatedGuard ],
|
||||||
|
path: `${PUBLICATION_CLAIMS_PATH}`,
|
||||||
|
component: AdminNotificationsPublicationClaimPageComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'admin.notifications.publicationclaim.page.title',
|
||||||
|
breadcrumbKey: 'admin.notifications.publicationclaim',
|
||||||
|
showBreadcrumbsFluid: false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
canActivate: [ AuthenticatedGuard ],
|
canActivate: [ AuthenticatedGuard ],
|
||||||
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
|
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
|
||||||
@@ -71,7 +89,9 @@ import {
|
|||||||
providers: [
|
providers: [
|
||||||
I18nBreadcrumbResolver,
|
I18nBreadcrumbResolver,
|
||||||
I18nBreadcrumbsService,
|
I18nBreadcrumbsService,
|
||||||
|
AdminNotificationsPublicationClaimPageResolver,
|
||||||
SourceDataResolver,
|
SourceDataResolver,
|
||||||
|
AdminQualityAssuranceSourcePageResolver,
|
||||||
AdminQualityAssuranceTopicsPageResolver,
|
AdminQualityAssuranceTopicsPageResolver,
|
||||||
AdminQualityAssuranceEventsPageResolver,
|
AdminQualityAssuranceEventsPageResolver,
|
||||||
AdminQualityAssuranceSourcePageResolver,
|
AdminQualityAssuranceSourcePageResolver,
|
||||||
|
@@ -3,10 +3,11 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CoreModule } from '../../core/core.module';
|
import { CoreModule } from '../../core/core.module';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
|
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
|
||||||
|
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
|
||||||
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
||||||
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
||||||
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
|
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
|
||||||
import {NotificationsModule} from '../../notifications/notifications.module';
|
import { NotificationsModule } from '../../notifications/notifications.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,6 +18,7 @@ import {NotificationsModule} from '../../notifications/notifications.module';
|
|||||||
NotificationsModule
|
NotificationsModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
AdminNotificationsPublicationClaimPageComponent,
|
||||||
AdminQualityAssuranceTopicsPageComponent,
|
AdminQualityAssuranceTopicsPageComponent,
|
||||||
AdminQualityAssuranceEventsPageComponent,
|
AdminQualityAssuranceEventsPageComponent,
|
||||||
AdminQualityAssuranceSourcePageComponent
|
AdminQualityAssuranceSourcePageComponent
|
||||||
|
@@ -38,6 +38,7 @@ import {
|
|||||||
ThemedPageInternalServerErrorComponent
|
ThemedPageInternalServerErrorComponent
|
||||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
|
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
|
||||||
import { MenuResolver } from './menu.resolver';
|
import { MenuResolver } from './menu.resolver';
|
||||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||||
import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
|
import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
|
||||||
@@ -206,6 +207,11 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
|
|||||||
.then((m) => m.ProcessPageModule),
|
.then((m) => m.ProcessPageModule),
|
||||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
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,
|
path: INFO_MODULE_PATH,
|
||||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
:host {
|
|
||||||
::ng-deep {
|
|
||||||
.switch {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--bs-spacer) * 2.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:host ::ng-deep ds-dynamic-form-control-container > div > label {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@@ -2,14 +2,35 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import {
|
||||||
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
|
combineLatest,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
Subscription
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormGroupModel,
|
||||||
|
DynamicFormLayout,
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicInputModel,
|
||||||
|
DynamicSelectModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
import {
|
||||||
|
DynamicCustomSwitchModel
|
||||||
|
} from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators';
|
import {
|
||||||
|
getAllSucceededRemoteDataPayload,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||||
@@ -245,7 +266,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* All input models in a simple array for easier iterations
|
* All input models in a simple array for easier iterations
|
||||||
*/
|
*/
|
||||||
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel,
|
inputModels = [this.primaryBitstreamModel, this.fileNameModel, this.descriptionModel, this.selectedFormatModel,
|
||||||
this.newFormatModel];
|
this.newFormatModel];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,8 +277,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
new DynamicFormGroupModel({
|
new DynamicFormGroupModel({
|
||||||
id: 'fileNamePrimaryContainer',
|
id: 'fileNamePrimaryContainer',
|
||||||
group: [
|
group: [
|
||||||
this.fileNameModel,
|
this.primaryBitstreamModel,
|
||||||
this.primaryBitstreamModel
|
this.fileNameModel
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
grid: {
|
grid: {
|
||||||
@@ -295,7 +316,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
primaryBitstream: {
|
primaryBitstream: {
|
||||||
grid: {
|
grid: {
|
||||||
host: 'col col-sm-4 d-inline-block switch border-0'
|
container: 'col-12'
|
||||||
|
},
|
||||||
|
element: {
|
||||||
|
container: 'text-right'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
|
@@ -25,7 +25,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
|||||||
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
||||||
import { BrowseByGuard } from '../browse-by/browse-by-guard';
|
import { BrowseByGuard } from '../browse-by/browse-by-guard';
|
||||||
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
||||||
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component';
|
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -73,7 +73,7 @@ import { CollectionRecentlyAddedComponent } from './sections/recently-added/coll
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
component: CollectionRecentlyAddedComponent,
|
component: ComcolSearchSectionComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'browse/:id',
|
path: 'browse/:id',
|
||||||
|
@@ -19,7 +19,6 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
|||||||
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
|
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
|
||||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
import { BrowseByPageModule } from '../browse-by/browse-by-page.module';
|
import { BrowseByPageModule } from '../browse-by/browse-by-page.module';
|
||||||
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component';
|
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
@@ -29,7 +28,6 @@ const DECLARATIONS = [
|
|||||||
EditItemTemplatePageComponent,
|
EditItemTemplatePageComponent,
|
||||||
ThemedEditItemTemplatePageComponent,
|
ThemedEditItemTemplatePageComponent,
|
||||||
CollectionItemMapperComponent,
|
CollectionItemMapperComponent,
|
||||||
CollectionRecentlyAddedComponent,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -88,7 +88,7 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
invoke: createSuccessfulRemoteDataObject$(process),
|
invoke: createSuccessfulRemoteDataObject$(process),
|
||||||
});
|
});
|
||||||
processDataService = jasmine.createSpyObj('processDataService', {
|
processDataService = jasmine.createSpyObj('processDataService', {
|
||||||
findById: createSuccessfulRemoteDataObject$(process),
|
autoRefreshUntilCompletion: createSuccessfulRemoteDataObject$(process),
|
||||||
});
|
});
|
||||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||||
findByHref: createSuccessfulRemoteDataObject$(bitstream),
|
findByHref: createSuccessfulRemoteDataObject$(bitstream),
|
||||||
@@ -137,7 +137,7 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
{name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)},
|
{name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)},
|
||||||
], []);
|
], []);
|
||||||
|
|
||||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId);
|
||||||
expect(bitstreamService.findByHref).toHaveBeenCalledWith(process._links.output.href);
|
expect(bitstreamService.findByHref).toHaveBeenCalledWith(process._links.output.href);
|
||||||
expect(notificationsService.info).toHaveBeenCalledWith(jasmine.anything() as any, 'Script text');
|
expect(notificationsService.info).toHaveBeenCalledWith(jasmine.anything() as any, 'Script text');
|
||||||
});
|
});
|
||||||
@@ -151,7 +151,7 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
{name: '-r', value: null},
|
{name: '-r', value: null},
|
||||||
{name: '-c', value: collection.uuid},
|
{name: '-c', value: collection.uuid},
|
||||||
], []);
|
], []);
|
||||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId);
|
||||||
expect(notificationsService.success).toHaveBeenCalled();
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -164,7 +164,7 @@ describe('CollectionSourceControlsComponent', () => {
|
|||||||
{name: '-o', value: null},
|
{name: '-o', value: null},
|
||||||
{name: '-c', value: collection.uuid},
|
{name: '-c', value: collection.uuid},
|
||||||
], []);
|
], []);
|
||||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
expect(processDataService.autoRefreshUntilCompletion).toHaveBeenCalledWith(process.processId);
|
||||||
expect(notificationsService.success).toHaveBeenCalled();
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
import { Component, Input, OnDestroy } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||||
import { ContentSource } from '../../../../core/shared/content-source.model';
|
import { ContentSource } from '../../../../core/shared/content-source.model';
|
||||||
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
|
||||||
import {
|
import {
|
||||||
getAllCompletedRemoteData,
|
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { filter, map, switchMap, tap } from 'rxjs/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 { ProcessStatus } from '../../../../process-page/processes/process-status.model';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
import { RequestService } from '../../../../core/data/request.service';
|
import { RequestService } from '../../../../core/data/request.service';
|
||||||
@@ -30,7 +29,7 @@ import { ContentSourceSetSerializer } from '../../../../core/shared/content-sour
|
|||||||
styleUrls: ['./collection-source-controls.component.scss'],
|
styleUrls: ['./collection-source-controls.component.scss'],
|
||||||
templateUrl: './collection-source-controls.component.html',
|
templateUrl: './collection-source-controls.component.html',
|
||||||
})
|
})
|
||||||
export class CollectionSourceControlsComponent implements OnDestroy {
|
export class CollectionSourceControlsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the controls be enabled.
|
* Should the controls be enabled.
|
||||||
@@ -49,6 +48,7 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
|
|
||||||
contentSource$: Observable<ContentSource>;
|
contentSource$: Observable<ContentSource>;
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
private autoRefreshIDs: string[] = [];
|
||||||
|
|
||||||
testConfigRunning$ = new BehaviorSubject(false);
|
testConfigRunning$ = new BehaviorSubject(false);
|
||||||
importRunning$ = new BehaviorSubject(false);
|
importRunning$ = new BehaviorSubject(false);
|
||||||
@@ -95,19 +95,12 @@ 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 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)),
|
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
switchMap((rd) => {
|
||||||
getAllCompletedRemoteData(),
|
this.autoRefreshIDs.push(rd.payload.processId);
|
||||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
|
||||||
map((rd) => rd.payload),
|
}),
|
||||||
hasValueOperator(),
|
map((rd) => rd.payload)
|
||||||
).subscribe((process: Process) => {
|
).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()) {
|
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||||
this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed'));
|
this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed'));
|
||||||
this.testConfigRunning$.next(false);
|
this.testConfigRunning$.next(false);
|
||||||
@@ -123,8 +116,7 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
this.testConfigRunning$.next(false);
|
this.testConfigRunning$.next(false);
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,20 +139,12 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
switchMap((rd) => {
|
||||||
getAllCompletedRemoteData(),
|
this.autoRefreshIDs.push(rd.payload.processId);
|
||||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
|
||||||
map((rd) => rd.payload),
|
}),
|
||||||
hasValueOperator(),
|
map((rd) => rd.payload)
|
||||||
).subscribe((process) => {
|
).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()) {
|
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||||
this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed'));
|
this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed'));
|
||||||
this.importRunning$.next(false);
|
this.importRunning$.next(false);
|
||||||
@@ -170,8 +154,7 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||||
this.importRunning$.next(false);
|
this.importRunning$.next(false);
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,20 +177,12 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
switchMap((rd) => {
|
||||||
getAllCompletedRemoteData(),
|
this.autoRefreshIDs.push(rd.payload.processId);
|
||||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
return this.processDataService.autoRefreshUntilCompletion(rd.payload.processId);
|
||||||
map((rd) => rd.payload),
|
}),
|
||||||
hasValueOperator(),
|
map((rd) => rd.payload)
|
||||||
).subscribe((process) => {
|
).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()) {
|
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||||
this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed'));
|
this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed'));
|
||||||
this.reImportRunning$.next(false);
|
this.reImportRunning$.next(false);
|
||||||
@@ -217,8 +192,7 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||||
this.reImportRunning$.next(false);
|
this.reImportRunning$.next(false);
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@@ -227,5 +201,9 @@ export class CollectionSourceControlsComponent implements OnDestroy {
|
|||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.autoRefreshIDs.forEach((id) => {
|
||||||
|
this.processDataService.stopAutoRefreshing(id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
|
||||||
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
|
||||||
<h3 class="sr-only">{{'collection.page.browse.recent.head' | translate}}</h3>
|
|
||||||
<ds-viewable-collection
|
|
||||||
[config]="paginationConfig"
|
|
||||||
[sortConfig]="sortConfig"
|
|
||||||
[objects]="itemRD"
|
|
||||||
[hideGear]="true">
|
|
||||||
</ds-viewable-collection>
|
|
||||||
</div>
|
|
||||||
<ds-error *ngIf="itemRD?.hasFailed"
|
|
||||||
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
|
||||||
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
|
|
||||||
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
|
|
||||||
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
|
|
||||||
{{'collection.page.browse.recent.empty' | translate}}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
@@ -1,53 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { CollectionRecentlyAddedComponent } from './collection-recently-added.component';
|
|
||||||
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
|
||||||
import { environment } from '../../../../environments/environment.test';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
|
||||||
import { SearchServiceStub } from '../../../shared/testing/search-service.stub';
|
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
||||||
|
|
||||||
describe('CollectionRecentlyAddedComponent', () => {
|
|
||||||
let component: CollectionRecentlyAddedComponent;
|
|
||||||
let fixture: ComponentFixture<CollectionRecentlyAddedComponent>;
|
|
||||||
|
|
||||||
let activatedRoute: ActivatedRouteStub;
|
|
||||||
let paginationService: PaginationServiceStub;
|
|
||||||
let searchService: SearchServiceStub;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
activatedRoute = new ActivatedRouteStub();
|
|
||||||
paginationService = new PaginationServiceStub();
|
|
||||||
searchService = new SearchServiceStub();
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [
|
|
||||||
CollectionRecentlyAddedComponent,
|
|
||||||
VarDirective,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
|
||||||
{ provide: SearchService, useValue: SearchServiceStub },
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(CollectionRecentlyAddedComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,82 +0,0 @@
|
|||||||
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
|
|
||||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
|
||||||
import { switchMap, map, startWith, take } from 'rxjs/operators';
|
|
||||||
import { getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
|
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
|
||||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
|
||||||
import { BROWSE_LINKS_TO_FOLLOW } from '../../../core/browse/browse.service';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortOptions, SortDirection } from '../../../core/cache/models/sort-options.model';
|
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
import { ActivatedRoute, Data } from '@angular/router';
|
|
||||||
import { fadeIn } from '../../../shared/animations/fade';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-collection-recently-added',
|
|
||||||
templateUrl: './collection-recently-added.component.html',
|
|
||||||
styleUrls: ['./collection-recently-added.component.scss'],
|
|
||||||
animations: [fadeIn],
|
|
||||||
})
|
|
||||||
export class CollectionRecentlyAddedComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
paginationConfig: PaginationComponentOptions;
|
|
||||||
|
|
||||||
sortConfig: SortOptions;
|
|
||||||
|
|
||||||
collectionRD$: Observable<RemoteData<Collection>>;
|
|
||||||
|
|
||||||
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
|
||||||
protected paginationService: PaginationService,
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
protected searchService: SearchService,
|
|
||||||
) {
|
|
||||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
|
||||||
id: 'cp',
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: this.appConfig.browseBy.pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
|
||||||
map((data: Data) => data.dso as RemoteData<Collection>),
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.itemRD$ = observableCombineLatest([
|
|
||||||
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig),
|
|
||||||
this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig),
|
|
||||||
]).pipe(
|
|
||||||
switchMap(([currentPagination, currentSort]: [PaginationComponentOptions, SortOptions]) => this.collectionRD$.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
map((rd: RemoteData<Collection>) => rd.payload.id),
|
|
||||||
switchMap((id: string) => this.searchService.search<Item>(
|
|
||||||
new PaginatedSearchOptions({
|
|
||||||
scope: id,
|
|
||||||
pagination: currentPagination,
|
|
||||||
sort: currentSort,
|
|
||||||
dsoTypes: [DSpaceObjectType.ITEM]
|
|
||||||
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe(
|
|
||||||
toDSpaceObjectListRD()
|
|
||||||
) as Observable<RemoteData<PaginatedList<Item>>>),
|
|
||||||
startWith(undefined), // Make sure switching pages shows loading component
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -19,6 +19,8 @@ import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-co
|
|||||||
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
||||||
import { BrowseByGuard } from '../browse-by/browse-by-guard';
|
import { BrowseByGuard } from '../browse-by/browse-by-guard';
|
||||||
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
||||||
|
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -56,7 +58,16 @@ import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
|
component: ComcolSearchSectionComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subcoms-cols',
|
||||||
|
pathMatch: 'full',
|
||||||
component: SubComColSectionComponent,
|
component: SubComColSectionComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: { breadcrumbKey: 'community.subcoms-cols' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'browse/:id',
|
path: 'browse/:id',
|
||||||
|
@@ -7,7 +7,7 @@ import { Bitstream } from '../core/shared/bitstream.model';
|
|||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { fadeInOut } from '../shared/animations/fade';
|
import { fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
@@ -11,6 +11,7 @@ describe('SubComColSectionComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
activatedRoute = new ActivatedRouteStub();
|
activatedRoute = new ActivatedRouteStub();
|
||||||
|
activatedRoute.parent = new ActivatedRouteStub();
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@@ -20,7 +20,7 @@ export class SubComColSectionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.community$ = this.route.data.pipe(
|
this.community$ = this.route.parent.data.pipe(
|
||||||
map((data: Data) => (data.dso as RemoteData<Community>).payload),
|
map((data: Data) => (data.dso as RemoteData<Community>).payload),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver';
|
||||||
|
|
||||||
|
describe('PublicationClaimBreadcrumbResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: PublicationClaimBreadcrumbResolver;
|
||||||
|
let publicationClaimBreadcrumbService: any;
|
||||||
|
const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
|
||||||
|
const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a';
|
||||||
|
const expectedId = 'openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
|
||||||
|
let route;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
route = {
|
||||||
|
paramMap: {
|
||||||
|
get: function (param) {
|
||||||
|
return this[param];
|
||||||
|
},
|
||||||
|
targetId: expectedId,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
publicationClaimBreadcrumbService = {};
|
||||||
|
resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the breadcrumb config', () => {
|
||||||
|
const resolvedConfig = resolver.resolve(route as any, {url: fullPath } as any);
|
||||||
|
const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey };
|
||||||
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import {BreadcrumbConfig} from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PublicationClaimBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
||||||
|
constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that resolve Publication Claim item into a breadcrumb
|
||||||
|
* The parameter are retrieved by the url since part of the Publication Claim route config
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
|
||||||
|
const targetId = route.paramMap.get('targetId').split(':')[1];
|
||||||
|
return { provider: this.breadcrumbService, key: targetId };
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
describe('PublicationClaimBreadcrumbService', () => {
|
||||||
|
let service: PublicationClaimBreadcrumbService;
|
||||||
|
let dsoNameService: any = {
|
||||||
|
getName: (str) => str
|
||||||
|
};
|
||||||
|
let translateService: any = {
|
||||||
|
instant: (str) => str,
|
||||||
|
};
|
||||||
|
|
||||||
|
let dataService: any = {
|
||||||
|
findById: (str) => createSuccessfulRemoteDataObject$(str),
|
||||||
|
};
|
||||||
|
|
||||||
|
let authorizationService: any = {
|
||||||
|
isAuthorized: (str) => of(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let exampleKey;
|
||||||
|
|
||||||
|
const ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim';
|
||||||
|
const ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
exampleKey = 'suggestion.suggestionFor.breadcrumb';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new PublicationClaimBreadcrumbService(dataService,dsoNameService,translateService, authorizationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBreadcrumbs', () => {
|
||||||
|
it('should return a breadcrumb based on a string', () => {
|
||||||
|
const breadcrumbs = service.getBreadcrumbs(exampleKey);
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY, ADMIN_PUBLICATION_CLAIMS_PATH),
|
||||||
|
new Breadcrumb(exampleKey, undefined)]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
|
||||||
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { DSONameService } from './dso-name.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate Publication claims breadcrumbs
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderService<string> {
|
||||||
|
private ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim';
|
||||||
|
private ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title';
|
||||||
|
|
||||||
|
constructor(private dataService: ItemDataService,
|
||||||
|
private dsoNameService: DSONameService,
|
||||||
|
private tranlsateService: TranslateService,
|
||||||
|
protected authorizationService: AuthorizationDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to calculate the breadcrumbs
|
||||||
|
* @param key The key used to resolve the breadcrumb
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(key: string): Observable<Breadcrumb[]> {
|
||||||
|
return combineLatest([this.dataService.findById(key).pipe(getFirstCompletedRemoteData()),this.authorizationService.isAuthorized(FeatureID.AdministratorOf)]).pipe(
|
||||||
|
map(([item, isAdmin]) => {
|
||||||
|
const itemName = this.dsoNameService.getName(item.payload);
|
||||||
|
return isAdmin ? [new Breadcrumb(this.tranlsateService.instant(this.ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY), this.ADMIN_PUBLICATION_CLAIMS_PATH),
|
||||||
|
new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)] :
|
||||||
|
[new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { getLinkDefinition, link } from './build-decorators';
|
import { dataService, getDataServiceFor, getLinkDefinition, link } from './build-decorators';
|
||||||
|
|
||||||
class TestHALResource implements HALResource {
|
class TestHALResource implements HALResource {
|
||||||
_links: {
|
_links: {
|
||||||
@@ -46,5 +46,17 @@ describe('build decorators', () => {
|
|||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`set data service`, () => {
|
||||||
|
it(`should throw error`, () => {
|
||||||
|
expect(dataService(null)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should set properly data service for type`, () => {
|
||||||
|
const target = new TestHALResource();
|
||||||
|
dataService(testType)(target);
|
||||||
|
expect(getDataServiceFor(testType)).toEqual(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
47
src/app/core/cache/builders/build-decorators.ts
vendored
47
src/app/core/cache/builders/build-decorators.ts
vendored
@@ -3,13 +3,20 @@ import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
|||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { getResourceTypeValueFor } from '../object-cache.reducer';
|
import {
|
||||||
|
getResourceTypeValueFor
|
||||||
|
} from '../object-cache.reducer';
|
||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { CacheableObject } from '../cacheable-object.model';
|
||||||
import { TypedObject } from '../typed-object.model';
|
import { TypedObject } from '../typed-object.model';
|
||||||
|
|
||||||
|
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getDataServiceFor
|
||||||
|
});
|
||||||
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
|
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => getLinkDefinition,
|
factory: () => getLinkDefinition
|
||||||
});
|
});
|
||||||
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
|
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -20,6 +27,7 @@ const resolvedLinkKey = Symbol('resolvedLink');
|
|||||||
|
|
||||||
const resolvedLinkMap = new Map();
|
const resolvedLinkMap = new Map();
|
||||||
const typeMap = new Map();
|
const typeMap = new Map();
|
||||||
|
const dataServiceMap = new Map();
|
||||||
const linkMap = new Map();
|
const linkMap = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +46,39 @@ export function getClassForType(type: string | ResourceType) {
|
|||||||
return typeMap.get(getResourceTypeValueFor(type));
|
return typeMap.get(getResourceTypeValueFor(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class decorator to indicate that this class is a dataservice
|
||||||
|
* for a given resource type.
|
||||||
|
*
|
||||||
|
* "dataservice" in this context means that it has findByHref and
|
||||||
|
* findAllByHref methods.
|
||||||
|
*
|
||||||
|
* @param resourceType the resource type the class is a dataservice for
|
||||||
|
*/
|
||||||
|
export function dataService(resourceType: ResourceType): any {
|
||||||
|
return (target: any) => {
|
||||||
|
if (hasNoValue(resourceType)) {
|
||||||
|
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
|
||||||
|
}
|
||||||
|
const existingDataservice = dataServiceMap.get(resourceType.value);
|
||||||
|
|
||||||
|
if (hasValue(existingDataservice)) {
|
||||||
|
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataServiceMap.set(resourceType.value, target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the dataservice matching the given resource type
|
||||||
|
*
|
||||||
|
* @param resourceType the resource type you want the matching dataservice for
|
||||||
|
*/
|
||||||
|
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
|
||||||
|
return dataServiceMap.get(resourceType.value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to represent the data that can be set by the @link decorator
|
* A class to represent the data that can be set by the @link decorator
|
||||||
*/
|
*/
|
||||||
@@ -65,7 +106,7 @@ export const link = <T extends HALResource>(
|
|||||||
resourceType: ResourceType,
|
resourceType: ResourceType,
|
||||||
isList = false,
|
isList = false,
|
||||||
linkName?: keyof T['_links'],
|
linkName?: keyof T['_links'],
|
||||||
) => {
|
) => {
|
||||||
return (target: T, propertyName: string) => {
|
return (target: T, propertyName: string) => {
|
||||||
let targetMap = linkMap.get(target.constructor);
|
let targetMap = linkMap.get(target.constructor);
|
||||||
|
|
||||||
|
@@ -161,6 +161,7 @@ export class RemoteDataBuildService {
|
|||||||
} else {
|
} else {
|
||||||
// in case the elements of the paginated list were already filled in, because they're UnCacheableObjects
|
// in case the elements of the paginated list were already filled in, because they're UnCacheableObjects
|
||||||
paginatedList.page = paginatedList.page
|
paginatedList.page = paginatedList.page
|
||||||
|
.filter((obj: any) => obj != null)
|
||||||
.map((obj: any) => this.plainObjectToInstance<T>(obj))
|
.map((obj: any) => this.plainObjectToInstance<T>(obj))
|
||||||
.map((obj: any) =>
|
.map((obj: any) =>
|
||||||
this.linkService.resolveLinks(obj, ...pageLink.linksToFollow)
|
this.linkService.resolveLinks(obj, ...pageLink.linksToFollow)
|
||||||
@@ -272,12 +273,13 @@ export class RemoteDataBuildService {
|
|||||||
return isStale(r2.state) ? r1 : r2;
|
return isStale(r2.state) ? r1 : r2;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
distinctUntilKeyChanged('lastUpdated')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload$ = this.buildPayload<T>(requestEntry$, href$, ...linksToFollow);
|
const payload$ = this.buildPayload<T>(requestEntry$, href$, ...linksToFollow);
|
||||||
|
|
||||||
return this.toRemoteDataObservable<T>(requestEntry$, payload$);
|
return this.toRemoteDataObservable<T>(requestEntry$, payload$).pipe(
|
||||||
|
distinctUntilKeyChanged('lastUpdated'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -185,6 +185,8 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
|
|||||||
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
|
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
|
||||||
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
|
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
|
||||||
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
|
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
|
||||||
|
import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model';
|
||||||
|
import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -386,7 +388,9 @@ export const models =
|
|||||||
IdentifierData,
|
IdentifierData,
|
||||||
Subscription,
|
Subscription,
|
||||||
ItemRequest,
|
ItemRequest,
|
||||||
BulkAccessConditionOptions
|
BulkAccessConditionOptions,
|
||||||
|
SuggestionTarget,
|
||||||
|
SuggestionSource
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -21,6 +21,10 @@ import { RequestEntryState } from '../request-entry-state.model';
|
|||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { BaseDataService } from './base-data.service';
|
import { BaseDataService } from './base-data.service';
|
||||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
|
||||||
|
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -46,34 +50,18 @@ describe('BaseDataService', () => {
|
|||||||
let requestService;
|
let requestService;
|
||||||
let halService;
|
let halService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
let objectCache;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let selfLink;
|
let selfLink;
|
||||||
let linksToFollow;
|
let linksToFollow;
|
||||||
let testScheduler;
|
let testScheduler;
|
||||||
let remoteDataMocks;
|
let remoteDataMocks: { [responseType: string]: RemoteData<any> };
|
||||||
|
let remoteDataPageMocks: { [responseType: string]: RemoteData<any> };
|
||||||
|
|
||||||
function initTestService(): TestService {
|
function initTestService(): TestService {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
halService = new HALEndpointServiceStub('url') as any;
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
objectCache = {
|
objectCache = new ObjectCacheServiceStub();
|
||||||
|
|
||||||
addPatch: () => {
|
|
||||||
/* empty */
|
|
||||||
},
|
|
||||||
getObjectBySelfLink: () => {
|
|
||||||
/* empty */
|
|
||||||
},
|
|
||||||
getByHref: () => {
|
|
||||||
/* empty */
|
|
||||||
},
|
|
||||||
addDependency: () => {
|
|
||||||
/* empty */
|
|
||||||
},
|
|
||||||
removeDependents: () => {
|
|
||||||
/* empty */
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
linksToFollow = [
|
linksToFollow = [
|
||||||
followLink('a'),
|
followLink('a'),
|
||||||
@@ -88,7 +76,27 @@ describe('BaseDataService', () => {
|
|||||||
|
|
||||||
const timeStamp = new Date().getTime();
|
const timeStamp = new Date().getTime();
|
||||||
const msToLive = 15 * 60 * 1000;
|
const msToLive = 15 * 60 * 1000;
|
||||||
const payload = { foo: 'bar' };
|
const payload = {
|
||||||
|
foo: 'bar',
|
||||||
|
followLink1: {},
|
||||||
|
followLink2: {},
|
||||||
|
_links: {
|
||||||
|
self: Object.assign(new HALLink(), {
|
||||||
|
href: 'self-test-link',
|
||||||
|
}),
|
||||||
|
followLink1: Object.assign(new HALLink(), {
|
||||||
|
href: 'follow-link-1',
|
||||||
|
}),
|
||||||
|
followLink2: [
|
||||||
|
Object.assign(new HALLink(), {
|
||||||
|
href: 'follow-link-2-1',
|
||||||
|
}),
|
||||||
|
Object.assign(new HALLink(), {
|
||||||
|
href: 'follow-link-2-2',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
||||||
const statusCodeSuccess = 200;
|
const statusCodeSuccess = 200;
|
||||||
const statusCodeError = 404;
|
const statusCodeError = 404;
|
||||||
const errorMessage = 'not found';
|
const errorMessage = 'not found';
|
||||||
@@ -101,11 +109,20 @@ describe('BaseDataService', () => {
|
|||||||
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
};
|
};
|
||||||
|
remoteDataPageMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
||||||
|
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess),
|
||||||
|
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||||
|
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||||
|
};
|
||||||
|
|
||||||
return new TestService(
|
return new TestService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -380,6 +397,27 @@ describe('BaseDataService', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should link all the followLinks of a cached object by calling addDependency', () => {
|
||||||
|
spyOn(objectCache, 'addDependency').and.callThrough();
|
||||||
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d', {
|
||||||
|
a: remoteDataMocks.Success,
|
||||||
|
b: remoteDataMocks.RequestPending,
|
||||||
|
c: remoteDataMocks.ResponsePending,
|
||||||
|
d: remoteDataMocks.Success,
|
||||||
|
}));
|
||||||
|
const expected = '--b-c-d';
|
||||||
|
const values = {
|
||||||
|
b: remoteDataMocks.RequestPending,
|
||||||
|
c: remoteDataMocks.ResponsePending,
|
||||||
|
d: remoteDataMocks.Success,
|
||||||
|
};
|
||||||
|
|
||||||
|
expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values);
|
||||||
|
flush();
|
||||||
|
expect(objectCache.addDependency).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`findListByHref`, () => {
|
describe(`findListByHref`, () => {
|
||||||
@@ -392,8 +430,8 @@ describe('BaseDataService', () => {
|
|||||||
it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => {
|
it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => {
|
||||||
testScheduler.run(({ cold }) => {
|
testScheduler.run(({ cold }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
|
|
||||||
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
|
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
|
||||||
@@ -403,8 +441,8 @@ describe('BaseDataService', () => {
|
|||||||
it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => {
|
it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
|
|
||||||
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
||||||
@@ -419,8 +457,8 @@ describe('BaseDataService', () => {
|
|||||||
it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => {
|
it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
|
|
||||||
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
||||||
@@ -431,12 +469,12 @@ describe('BaseDataService', () => {
|
|||||||
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => {
|
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.SuccessStale }));
|
||||||
|
|
||||||
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||||
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
||||||
spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale }));
|
||||||
// prove that the spy we just added hasn't been called yet
|
// prove that the spy we just added hasn't been called yet
|
||||||
expect(service.findListByHref).not.toHaveBeenCalled();
|
expect(service.findListByHref).not.toHaveBeenCalled();
|
||||||
// call the callback passed to reRequestStaleRemoteData
|
// call the callback passed to reRequestStaleRemoteData
|
||||||
@@ -451,7 +489,7 @@ describe('BaseDataService', () => {
|
|||||||
it(`should return a the output from reRequestStaleRemoteData`, () => {
|
it(`should return a the output from reRequestStaleRemoteData`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
|
||||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' }));
|
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' }));
|
||||||
const expected = 'a';
|
const expected = 'a';
|
||||||
const values = {
|
const values = {
|
||||||
@@ -471,19 +509,19 @@ describe('BaseDataService', () => {
|
|||||||
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => {
|
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||||
a: remoteDataMocks.Success,
|
a: remoteDataPageMocks.Success,
|
||||||
b: remoteDataMocks.RequestPending,
|
b: remoteDataPageMocks.RequestPending,
|
||||||
c: remoteDataMocks.ResponsePending,
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
d: remoteDataMocks.Success,
|
d: remoteDataPageMocks.Success,
|
||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataPageMocks.SuccessStale,
|
||||||
}));
|
}));
|
||||||
const expected = 'a-b-c-d-e';
|
const expected = 'a-b-c-d-e';
|
||||||
const values = {
|
const values = {
|
||||||
a: remoteDataMocks.Success,
|
a: remoteDataPageMocks.Success,
|
||||||
b: remoteDataMocks.RequestPending,
|
b: remoteDataPageMocks.RequestPending,
|
||||||
c: remoteDataMocks.ResponsePending,
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
d: remoteDataMocks.Success,
|
d: remoteDataPageMocks.Success,
|
||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataPageMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||||
@@ -493,20 +531,20 @@ describe('BaseDataService', () => {
|
|||||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
||||||
a: remoteDataMocks.ResponsePendingStale,
|
a: remoteDataPageMocks.ResponsePendingStale,
|
||||||
b: remoteDataMocks.SuccessStale,
|
b: remoteDataPageMocks.SuccessStale,
|
||||||
c: remoteDataMocks.ErrorStale,
|
c: remoteDataPageMocks.ErrorStale,
|
||||||
d: remoteDataMocks.RequestPending,
|
d: remoteDataPageMocks.RequestPending,
|
||||||
e: remoteDataMocks.ResponsePending,
|
e: remoteDataPageMocks.ResponsePending,
|
||||||
f: remoteDataMocks.Success,
|
f: remoteDataPageMocks.Success,
|
||||||
g: remoteDataMocks.SuccessStale,
|
g: remoteDataPageMocks.SuccessStale,
|
||||||
}));
|
}));
|
||||||
const expected = '------d-e-f-g';
|
const expected = '------d-e-f-g';
|
||||||
const values = {
|
const values = {
|
||||||
d: remoteDataMocks.RequestPending,
|
d: remoteDataPageMocks.RequestPending,
|
||||||
e: remoteDataMocks.ResponsePending,
|
e: remoteDataPageMocks.ResponsePending,
|
||||||
f: remoteDataMocks.Success,
|
f: remoteDataPageMocks.Success,
|
||||||
g: remoteDataMocks.SuccessStale,
|
g: remoteDataPageMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||||
@@ -525,18 +563,18 @@ describe('BaseDataService', () => {
|
|||||||
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||||
a: remoteDataMocks.Success,
|
a: remoteDataPageMocks.Success,
|
||||||
b: remoteDataMocks.RequestPending,
|
b: remoteDataPageMocks.RequestPending,
|
||||||
c: remoteDataMocks.ResponsePending,
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
d: remoteDataMocks.Success,
|
d: remoteDataPageMocks.Success,
|
||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataPageMocks.SuccessStale,
|
||||||
}));
|
}));
|
||||||
const expected = '--b-c-d-e';
|
const expected = '--b-c-d-e';
|
||||||
const values = {
|
const values = {
|
||||||
b: remoteDataMocks.RequestPending,
|
b: remoteDataPageMocks.RequestPending,
|
||||||
c: remoteDataMocks.ResponsePending,
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
d: remoteDataMocks.Success,
|
d: remoteDataPageMocks.Success,
|
||||||
e: remoteDataMocks.SuccessStale,
|
e: remoteDataPageMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||||
@@ -546,20 +584,20 @@ describe('BaseDataService', () => {
|
|||||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
|
||||||
a: remoteDataMocks.ResponsePendingStale,
|
a: remoteDataPageMocks.ResponsePendingStale,
|
||||||
b: remoteDataMocks.SuccessStale,
|
b: remoteDataPageMocks.SuccessStale,
|
||||||
c: remoteDataMocks.ErrorStale,
|
c: remoteDataPageMocks.ErrorStale,
|
||||||
d: remoteDataMocks.RequestPending,
|
d: remoteDataPageMocks.RequestPending,
|
||||||
e: remoteDataMocks.ResponsePending,
|
e: remoteDataPageMocks.ResponsePending,
|
||||||
f: remoteDataMocks.Success,
|
f: remoteDataPageMocks.Success,
|
||||||
g: remoteDataMocks.SuccessStale,
|
g: remoteDataPageMocks.SuccessStale,
|
||||||
}));
|
}));
|
||||||
const expected = '------d-e-f-g';
|
const expected = '------d-e-f-g';
|
||||||
const values = {
|
const values = {
|
||||||
d: remoteDataMocks.RequestPending,
|
d: remoteDataPageMocks.RequestPending,
|
||||||
e: remoteDataMocks.ResponsePending,
|
e: remoteDataPageMocks.ResponsePending,
|
||||||
f: remoteDataMocks.Success,
|
f: remoteDataPageMocks.Success,
|
||||||
g: remoteDataMocks.SuccessStale,
|
g: remoteDataPageMocks.SuccessStale,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -567,6 +605,27 @@ describe('BaseDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should link all the followLinks of the cached objects by calling addDependency', () => {
|
||||||
|
spyOn(objectCache, 'addDependency').and.callThrough();
|
||||||
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d', {
|
||||||
|
a: remoteDataPageMocks.SuccessStale,
|
||||||
|
b: remoteDataPageMocks.RequestPending,
|
||||||
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
|
d: remoteDataPageMocks.Success,
|
||||||
|
}));
|
||||||
|
const expected = '--b-c-d';
|
||||||
|
const values = {
|
||||||
|
b: remoteDataPageMocks.RequestPending,
|
||||||
|
c: remoteDataPageMocks.ResponsePending,
|
||||||
|
d: remoteDataPageMocks.Success,
|
||||||
|
};
|
||||||
|
|
||||||
|
expectObservable(service.findListByHref(selfLink, findListOptions, false, false, ...linksToFollow)).toBe(expected, values);
|
||||||
|
flush();
|
||||||
|
expect(objectCache.addDependency).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -577,7 +636,7 @@ describe('BaseDataService', () => {
|
|||||||
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
|
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
|
||||||
requestUUIDs: ['request1', 'request2', 'request3'],
|
requestUUIDs: ['request1', 'request2', 'request3'],
|
||||||
dependentRequestUUIDs: ['request4', 'request5']
|
dependentRequestUUIDs: ['request4', 'request5']
|
||||||
}));
|
} as ObjectCacheEntry));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -24,16 +24,17 @@ import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
|
|||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
import { HALDataService } from './hal-data-service.interface';
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
|
||||||
export const EMBED_SEPARATOR = '%2F';
|
export const EMBED_SEPARATOR = '%2F';
|
||||||
/**
|
/**
|
||||||
* Common functionality for data services.
|
* Common functionality for data services.
|
||||||
* Specific functionality that not all services would need
|
* Specific functionality that not all services would need
|
||||||
* is implemented in "DataService feature" classes (e.g. {@link CreateData}
|
* is implemented in "UpdateDataServiceImpl feature" classes (e.g. {@link CreateData}
|
||||||
*
|
*
|
||||||
* All DataService (or DataService feature) classes must
|
* All UpdateDataServiceImpl (or UpdateDataServiceImpl feature) classes must
|
||||||
* - extend this class (or {@link IdentifiableDataService})
|
* - extend this class (or {@link IdentifiableDataService})
|
||||||
* - implement any DataService features it requires in order to forward calls to it
|
* - implement any UpdateDataServiceImpl features it requires in order to forward calls to it
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* export class SomeDataService extends BaseDataService<Something> implements CreateData<Something>, SearchData<Something> {
|
* export class SomeDataService extends BaseDataService<Something> implements CreateData<Something>, SearchData<Something> {
|
||||||
@@ -268,7 +269,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
|
|
||||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
const response$: Observable<RemoteData<T>> = this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
// This skip ensures that if a stale object is present in the cache when you do a
|
// This skip ensures that if a stale object is present in the cache when you do a
|
||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
@@ -277,6 +278,25 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
);
|
);
|
||||||
|
return response$.pipe(
|
||||||
|
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
|
||||||
|
tap((remoteDataObject: RemoteData<T>) => {
|
||||||
|
if (hasValue(remoteDataObject?.payload?._links)) {
|
||||||
|
for (const followLinkName of Object.keys(remoteDataObject.payload._links)) {
|
||||||
|
// only add the followLinks if they are embedded
|
||||||
|
if (hasValue(remoteDataObject.payload[followLinkName]) && followLinkName !== 'self') {
|
||||||
|
// followLink can be either an individual HALLink or a HALLink[]
|
||||||
|
const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]);
|
||||||
|
for (const individualFollowLink of followLinksList) {
|
||||||
|
if (hasValue(individualFollowLink?.href)) {
|
||||||
|
this.addDependency(response$, individualFollowLink.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,7 +322,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
|
|
||||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
const response$: Observable<RemoteData<PaginatedList<T>>> = this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
||||||
// This skip ensures that if a stale object is present in the cache when you do a
|
// This skip ensures that if a stale object is present in the cache when you do a
|
||||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||||
@@ -311,6 +331,29 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
);
|
);
|
||||||
|
return response$.pipe(
|
||||||
|
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
|
||||||
|
tap((remoteDataObject: RemoteData<PaginatedList<T>>) => {
|
||||||
|
if (hasValue(remoteDataObject?.payload?.page)) {
|
||||||
|
for (const object of remoteDataObject.payload.page) {
|
||||||
|
if (hasValue(object?._links)) {
|
||||||
|
for (const followLinkName of Object.keys(object._links)) {
|
||||||
|
// only add the followLinks if they are embedded
|
||||||
|
if (hasValue(object[followLinkName]) && followLinkName !== 'self') {
|
||||||
|
// followLink can be either an individual HALLink or a HALLink[]
|
||||||
|
const followLinksList: HALLink[] = [].concat(object._links[followLinkName]);
|
||||||
|
for (const individualFollowLink of followLinksList) {
|
||||||
|
if (hasValue(individualFollowLink?.href)) {
|
||||||
|
this.addDependency(response$, individualFollowLink.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,7 +428,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the links to traverse from the root of the api to the
|
* Return the links to traverse from the root of the api to the
|
||||||
* endpoint this DataService represents
|
* endpoint this UpdateDataServiceImpl represents
|
||||||
*
|
*
|
||||||
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
||||||
* links to 'bar' the linkPath for the BarDataService would be
|
* links to 'bar' the linkPath for the BarDataService would be
|
||||||
|
@@ -37,7 +37,7 @@ export interface CreateData<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService feature to create objects.
|
* A UpdateDataServiceImpl feature to create objects.
|
||||||
*
|
*
|
||||||
* Concrete data services can use this feature by implementing {@link CreateData}
|
* Concrete data services can use this feature by implementing {@link CreateData}
|
||||||
* and delegating its method to an inner instance of this class.
|
* and delegating its method to an inner instance of this class.
|
||||||
|
@@ -42,7 +42,7 @@ export interface FindAllData<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService feature to list all objects.
|
* A UpdateDataServiceImpl feature to list all objects.
|
||||||
*
|
*
|
||||||
* Concrete data services can use this feature by implementing {@link FindAllData}
|
* Concrete data services can use this feature by implementing {@link FindAllData}
|
||||||
* and delegating its method to an inner instance of this class.
|
* and delegating its method to an inner instance of this class.
|
||||||
|
@@ -54,7 +54,7 @@ export interface PatchData<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService feature to patch and update objects.
|
* A UpdateDataServiceImpl feature to patch and update objects.
|
||||||
*
|
*
|
||||||
* Concrete data services can use this feature by implementing {@link PatchData}
|
* Concrete data services can use this feature by implementing {@link PatchData}
|
||||||
* and delegating its method to an inner instance of this class.
|
* and delegating its method to an inner instance of this class.
|
||||||
|
@@ -31,7 +31,7 @@ export interface PutData<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService feature to send PUT requests.
|
* A UpdateDataServiceImpl feature to send PUT requests.
|
||||||
*
|
*
|
||||||
* Concrete data services can use this feature by implementing {@link PutData}
|
* Concrete data services can use this feature by implementing {@link PutData}
|
||||||
* and delegating its method to an inner instance of this class.
|
* and delegating its method to an inner instance of this class.
|
||||||
|
@@ -51,7 +51,7 @@ export interface SearchData<T extends CacheableObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService feature to search for objects.
|
* A UpdateDataServiceImpl feature to search for objects.
|
||||||
*
|
*
|
||||||
* Concrete data services can use this feature by implementing {@link SearchData}
|
* Concrete data services can use this feature by implementing {@link SearchData}
|
||||||
* and delegating its method to an inner instance of this class.
|
* and delegating its method to an inner instance of this class.
|
||||||
|
@@ -21,6 +21,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import objectContaining = jasmine.objectContaining;
|
import objectContaining = jasmine.objectContaining;
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { BundleDataService } from './bundle-data.service';
|
||||||
|
import { ItemMock } from 'src/app/shared/mocks/item.mock';
|
||||||
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||||
|
import { Bundle } from '../shared/bundle.model';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
|
||||||
describe('BitstreamDataService', () => {
|
describe('BitstreamDataService', () => {
|
||||||
let service: BitstreamDataService;
|
let service: BitstreamDataService;
|
||||||
@@ -29,6 +34,7 @@ describe('BitstreamDataService', () => {
|
|||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let bitstreamFormatService: BitstreamFormatDataService;
|
let bitstreamFormatService: BitstreamFormatDataService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let bundleDataService: BundleDataService;
|
||||||
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
||||||
|
|
||||||
const bitstream1 = Object.assign(new Bitstream(), {
|
const bitstream1 = Object.assign(new Bitstream(), {
|
||||||
@@ -62,6 +68,7 @@ describe('BitstreamDataService', () => {
|
|||||||
bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', {
|
bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', {
|
||||||
getBrowseEndpoint: observableOf(bitstreamFormatHref)
|
getBrowseEndpoint: observableOf(bitstreamFormatHref)
|
||||||
});
|
});
|
||||||
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -76,6 +83,7 @@ describe('BitstreamDataService', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
service = TestBed.inject(BitstreamDataService);
|
service = TestBed.inject(BitstreamDataService);
|
||||||
|
bundleDataService = TestBed.inject(BundleDataService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('composition', () => {
|
describe('composition', () => {
|
||||||
@@ -118,6 +126,32 @@ describe('BitstreamDataService', () => {
|
|||||||
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findPrimaryBitstreamByItemAndName', () => {
|
||||||
|
it('should return primary bitstream', () => {
|
||||||
|
const exprected$ = cold('(a|)', { a: bitstream1} );
|
||||||
|
const bundle = Object.assign(new Bundle(), {
|
||||||
|
primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)),
|
||||||
|
});
|
||||||
|
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
|
||||||
|
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if primary bitstream has not be succeeded ', () => {
|
||||||
|
const exprected$ = cold('(a|)', { a: null} );
|
||||||
|
const bundle = Object.assign(new Bundle(), {
|
||||||
|
primaryBitstream: observableOf(createFailedRemoteDataObject()),
|
||||||
|
});
|
||||||
|
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle)));
|
||||||
|
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return EMPTY if nothing where found', () => {
|
||||||
|
const exprected$ = cold('(|)', {} );
|
||||||
|
spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject<Bundle>()));
|
||||||
|
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to delete multiple bitstreams', () => {
|
it('should be able to delete multiple bitstreams', () => {
|
||||||
service.removeMultiple([bitstream1, bitstream2]);
|
service.removeMultiple([bitstream1, bitstream2]);
|
||||||
|
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, EMPTY } from 'rxjs';
|
||||||
import { find, map, switchMap, take } from 'rxjs/operators';
|
import { find, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig, followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
@@ -34,6 +34,7 @@ import { NoContent } from '../shared/NoContent.model';
|
|||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { dataService } from './base/data-service.decorator';
|
import { dataService } from './base/data-service.decorator';
|
||||||
import { Operation, RemoveOperation } from 'fast-json-patch';
|
import { Operation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
@@ -201,6 +202,37 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
|||||||
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Make a request to get primary bitstream
|
||||||
|
* in all current use cases, and having it simplifies this method
|
||||||
|
*
|
||||||
|
* @param item the {@link Item} the {@link Bundle} is a part of
|
||||||
|
* @param bundleName the name of the {@link Bundle} we want to find
|
||||||
|
* {@link Bitstream}s for
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @return {Observable<Bitstream | null>}
|
||||||
|
* Return an observable that constains primary bitstream information or null
|
||||||
|
*/
|
||||||
|
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
|
||||||
|
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<Bundle>) => {
|
||||||
|
if (!rd.hasSucceeded) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
return rd.payload.primaryBitstream.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rdb: RemoteData<Bitstream>) => rdb.hasSucceeded ? rdb.payload : null)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a new FindListRequest with given search method
|
* Make a new FindListRequest with given search method
|
||||||
*
|
*
|
||||||
|
@@ -24,6 +24,7 @@ import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
|||||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
import { testPatchDataImplementation } from './base/patch-data.spec';
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
const collectionId = 'fake-collection-id';
|
const collectionId = 'fake-collection-id';
|
||||||
@@ -35,7 +36,7 @@ describe('CollectionDataService', () => {
|
|||||||
let translate: TranslateService;
|
let translate: TranslateService;
|
||||||
let notificationsService: any;
|
let notificationsService: any;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
|
|
||||||
const mockCollection1: Collection = Object.assign(new Collection(), {
|
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||||
@@ -205,14 +206,12 @@ describe('CollectionDataService', () => {
|
|||||||
buildFromRequestUUID: buildResponse$,
|
buildFromRequestUUID: buildResponse$,
|
||||||
buildSingle: buildResponse$
|
buildSingle: buildResponse$
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = new ObjectCacheServiceStub();
|
||||||
remove: jasmine.createSpy('remove')
|
|
||||||
});
|
|
||||||
halService = new HALEndpointServiceStub(url);
|
halService = new HALEndpointServiceStub(url);
|
||||||
notificationsService = new NotificationsServiceStub();
|
notificationsService = new NotificationsServiceStub();
|
||||||
translate = getMockTranslateService();
|
translate = getMockTranslateService();
|
||||||
|
|
||||||
service = new CollectionDataService(requestService, rdbService, objectCache, halService, null, notificationsService, null, null, translate);
|
service = new CollectionDataService(requestService, rdbService, objectCache as ObjectCacheService, halService, null, notificationsService, null, null, translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -17,8 +17,8 @@ import { HALDataService } from './base/hal-data-service.interface';
|
|||||||
import { dataService } from './base/data-service.decorator';
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataService with only findByHref methods. Its purpose is to be used for resources that don't
|
* A UpdateDataServiceImpl with only findByHref methods. Its purpose is to be used for resources that don't
|
||||||
* need to be retrieved by ID, or have any way to update them, but require a DataService in order
|
* need to be retrieved by ID, or have any way to update them, but require a UpdateDataServiceImpl in order
|
||||||
* for their links to be resolved by the LinkService.
|
* for their links to be resolved by the LinkService.
|
||||||
*
|
*
|
||||||
* an @dataService annotation can be added for any number of these resource types
|
* an @dataService annotation can be added for any number of these resource types
|
||||||
|
@@ -7,13 +7,187 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { testFindAllDataImplementation } from '../base/find-all-data.spec';
|
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 { testDeleteDataImplementation } from '../base/delete-data.spec';
|
||||||
|
import { waitForAsync, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
|
import { 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 { ReducerManager } from '@ngrx/store';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
|
||||||
|
import { BitstreamFormatDataService } from '../bitstream-format-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { testSearchDataImplementation } from '../base/search-data.spec';
|
||||||
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
|
import { FindListOptions } from '../find-list-options.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
describe('ProcessDataService', () => {
|
describe('ProcessDataService', () => {
|
||||||
|
let testScheduler;
|
||||||
|
|
||||||
|
const mockTimer = (fn: () => {}, interval: number) => {
|
||||||
|
fn();
|
||||||
|
return 555;
|
||||||
|
};
|
||||||
|
|
||||||
describe('composition', () => {
|
describe('composition', () => {
|
||||||
const initService = () => new ProcessDataService(null, null, null, null, null, null);
|
const initService = () => new ProcessDataService(null, null, null, null, null, null, null, null);
|
||||||
testFindAllDataImplementation(initService);
|
testFindAllDataImplementation(initService);
|
||||||
testDeleteDataImplementation(initService);
|
testDeleteDataImplementation(initService);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
|
let requestService = getMockRequestService();
|
||||||
|
let processDataService;
|
||||||
|
let remoteDataBuildService;
|
||||||
|
|
||||||
|
describe('autoRefreshUntilCompletion', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
ProcessDataService,
|
||||||
|
{ provide: RequestService, useValue: null },
|
||||||
|
{ 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 },
|
||||||
|
{ provide: TIMER_FACTORY, useValue: mockTimer },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
processDataService = TestBed.inject(ProcessDataService);
|
||||||
|
spyOn(processDataService, 'invalidateByHref');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not do any polling when the process is already completed', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
let completedProcess = new Process();
|
||||||
|
completedProcess.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
|
||||||
|
const completedProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess);
|
||||||
|
|
||||||
|
spyOn(processDataService, 'findById').and.returnValue(
|
||||||
|
cold('c', {
|
||||||
|
'c': completedProcessRD
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let process$ = processDataService.autoRefreshUntilCompletion('instantly');
|
||||||
|
expectObservable(process$).toBe('c', {
|
||||||
|
c: completedProcessRD
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(processDataService.findById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(processDataService.invalidateByHref).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('r 150ms c', {
|
||||||
|
'r': runningProcessRD,
|
||||||
|
'c': completedProcessRD
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let process$ = processDataService.autoRefreshUntilCompletion('foo', 100);
|
||||||
|
expectObservable(process$).toBe('r 150ms c', {
|
||||||
|
'r': runningProcessRD,
|
||||||
|
'c': completedProcessRD
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(processDataService.findById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autoRefreshingSearchBy', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
|
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 },
|
||||||
|
{ provide: TIMER_FACTORY, useValue: mockTimer },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
processDataService = TestBed.inject(ProcessDataService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should refresh after the specified interval', fakeAsync(() => {
|
||||||
|
const runningProcess = Object.assign(new Process(), {
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/processes/123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
runningProcess.processStatus = ProcessStatus.RUNNING;
|
||||||
|
|
||||||
|
const runningProcessPagination: PaginatedList<Process> = Object.assign(new PaginatedList(), {
|
||||||
|
page: [runningProcess],
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/processesList/456'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcessPagination);
|
||||||
|
|
||||||
|
spyOn(processDataService, 'searchBy').and.returnValue(
|
||||||
|
of(runningProcessRD)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(processDataService.searchBy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
let sub = processDataService.autoRefreshingSearchBy('id', 'byProperty', new FindListOptions(), 200).subscribe();
|
||||||
|
expect(processDataService.searchBy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
tick(250);
|
||||||
|
|
||||||
|
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone, Inject, InjectionToken } from '@angular/core';
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
import { Process } from '../../../process-page/processes/process.model';
|
import { Process } from '../../../process-page/processes/process.model';
|
||||||
import { PROCESS } from '../../../process-page/processes/process.resource-type';
|
import { PROCESS } from '../../../process-page/processes/process.resource-type';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap, filter, distinctUntilChanged, find } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../paginated-list.model';
|
import { PaginatedList } from '../paginated-list.model';
|
||||||
import { Bitstream } from '../../shared/bitstream.model';
|
import { Bitstream } from '../../shared/bitstream.model';
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
@@ -19,12 +19,29 @@ import { dataService } from '../base/data-service.decorator';
|
|||||||
import { DeleteData, DeleteDataImpl } from '../base/delete-data';
|
import { DeleteData, DeleteDataImpl } from '../base/delete-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { NoContent } from '../../shared/NoContent.model';
|
import { NoContent } from '../../shared/NoContent.model';
|
||||||
|
import { getAllCompletedRemoteData } from '../../shared/operators';
|
||||||
|
import { ProcessStatus } from 'src/app/process-page/processes/process-status.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { SearchData, SearchDataImpl } from '../base/search-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
@Injectable()
|
||||||
@dataService(PROCESS)
|
@dataService(PROCESS)
|
||||||
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> {
|
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process>, SearchData<Process> {
|
||||||
|
|
||||||
private findAllData: FindAllData<Process>;
|
private findAllData: FindAllData<Process>;
|
||||||
private deleteData: DeleteData<Process>;
|
private deleteData: DeleteData<Process>;
|
||||||
|
private searchData: SearchData<Process>;
|
||||||
|
protected activelyBeingPolled: Map<string, NodeJS.Timeout> = new Map();
|
||||||
|
protected subs: Map<string, Subscription> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -33,11 +50,30 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected notificationsService: NotificationsService,
|
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);
|
super('processes', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +113,71 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
|
|||||||
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param searchMethod The search method for the Process
|
||||||
|
* @param options The FindListOptions object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true.
|
||||||
|
* @param reRequestOnStale Whether the request should automatically be re-
|
||||||
|
* requested after the response becomes stale.
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should automatically be resolved.
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Process>>>}
|
||||||
|
* Return an observable that emits a paginated list of processes
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id The id for this auto-refreshing search. Used to stop
|
||||||
|
* auto-refreshing afterwards, and ensure we're not
|
||||||
|
* auto-refreshing the same thing multiple times.
|
||||||
|
* @param searchMethod The search method for the Process
|
||||||
|
* @param options The FindListOptions object
|
||||||
|
* @param pollingIntervalInMs The interval by which the search will be repeated
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should automatically be resolved.
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Process>>>}
|
||||||
|
* Return an observable that emits a paginated list of processes every interval
|
||||||
|
*/
|
||||||
|
autoRefreshingSearchBy(id: string, searchMethod: string, options?: FindListOptions, pollingIntervalInMs: number = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<PaginatedList<Process>>> {
|
||||||
|
|
||||||
|
const result$ = this.searchBy(searchMethod, options, true, true, ...linksToFollow).pipe(
|
||||||
|
getAllCompletedRemoteData()
|
||||||
|
);
|
||||||
|
|
||||||
|
const sub = result$.pipe(
|
||||||
|
filter(() =>
|
||||||
|
!this.activelyBeingPolled.has(id)
|
||||||
|
)
|
||||||
|
).subscribe((processListRd: RemoteData<PaginatedList<Process>>) => {
|
||||||
|
this.clearCurrentTimeout(id);
|
||||||
|
const nextTimeout = this.timer(() => {
|
||||||
|
this.activelyBeingPolled.delete(id);
|
||||||
|
this.requestService.setStaleByHrefSubstring(processListRd.payload._links.self.href);
|
||||||
|
}, pollingIntervalInMs);
|
||||||
|
|
||||||
|
this.activelyBeingPolled.set(id, nextTimeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subs.set(id, sub);
|
||||||
|
|
||||||
|
return result$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-refreshing the request with the given id
|
||||||
|
* @param id the id of the request to stop automatically refreshing
|
||||||
|
*/
|
||||||
|
stopAutoRefreshing(id: string) {
|
||||||
|
this.clearCurrentTimeout(id);
|
||||||
|
if (hasValue(this.subs.get(id))) {
|
||||||
|
this.subs.get(id).unsubscribe();
|
||||||
|
this.subs.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an existing object on the server
|
* Delete an existing object on the server
|
||||||
* @param objectId The id of the object to be removed
|
* @param objectId The id of the object to be removed
|
||||||
@@ -101,4 +202,74 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
|
|||||||
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the timeout for the given id, if that timeout exists
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected clearCurrentTimeout(id: string): void {
|
||||||
|
const timeout = this.activelyBeingPolled.get(id);
|
||||||
|
if (hasValue(timeout)) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
this.activelyBeingPolled.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll the process with the given ID, using the given interval, until that process either
|
||||||
|
* completes successfully or fails
|
||||||
|
*
|
||||||
|
* Return an Observable<RemoteData> 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 The ID of the {@link Process} to poll
|
||||||
|
* @param pollingIntervalInMs The interval for how often the request needs to be polled
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be
|
||||||
|
* automatically resolved
|
||||||
|
*/
|
||||||
|
public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<Process>> {
|
||||||
|
const process$: Observable<RemoteData<Process>> = this.findById(processId, true, true, ...linksToFollow)
|
||||||
|
.pipe(
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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<Process>) =>
|
||||||
|
!ProcessDataService.hasCompletedOrFailed(processRD.payload) &&
|
||||||
|
!this.activelyBeingPolled.has(processId)
|
||||||
|
)
|
||||||
|
).subscribe((processRD: RemoteData<Process>) => {
|
||||||
|
this.clearCurrentTimeout(processId);
|
||||||
|
if (processRD.hasSucceeded) {
|
||||||
|
const nextTimeout = this.timer(() => {
|
||||||
|
this.activelyBeingPolled.delete(processId);
|
||||||
|
this.invalidateByHref(processRD.payload._links.self.href);
|
||||||
|
}, pollingIntervalInMs);
|
||||||
|
|
||||||
|
this.activelyBeingPolled.set(processId, nextTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subs.set(processId, sub);
|
||||||
|
|
||||||
|
// 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<Process>) => ProcessDataService.hasCompletedOrFailed(processRD.payload))
|
||||||
|
).subscribe(() => {
|
||||||
|
this.stopAutoRefreshing(processId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return process$.pipe(
|
||||||
|
distinctUntilChanged((previous: RemoteData<Process>, current: RemoteData<Process>) =>
|
||||||
|
previous.payload?.processStatus === current.payload?.processStatus,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,7 @@ import { FindListOptions } from './find-list-options.model';
|
|||||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
import { MetadataValue } from '../shared/metadata.models';
|
import { MetadataValue } from '../shared/metadata.models';
|
||||||
import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('RelationshipDataService', () => {
|
describe('RelationshipDataService', () => {
|
||||||
let service: RelationshipDataService;
|
let service: RelationshipDataService;
|
||||||
@@ -114,14 +115,7 @@ describe('RelationshipDataService', () => {
|
|||||||
'href': buildList$,
|
'href': buildList$,
|
||||||
'https://rest.api/core/publication/relationships': relationships$
|
'https://rest.api/core/publication/relationships': relationships$
|
||||||
});
|
});
|
||||||
const objectCache = Object.assign({
|
const objectCache = new ObjectCacheServiceStub();
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
remove: () => {
|
|
||||||
},
|
|
||||||
hasBySelfLinkObservable: () => observableOf(false),
|
|
||||||
hasByHref$: () => observableOf(false)
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
}) as ObjectCacheService;
|
|
||||||
|
|
||||||
const itemService = jasmine.createSpyObj('itemService', {
|
const itemService = jasmine.createSpyObj('itemService', {
|
||||||
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
|
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
|
||||||
@@ -133,7 +127,7 @@ describe('RelationshipDataService', () => {
|
|||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
halService,
|
halService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
itemService,
|
itemService,
|
||||||
null,
|
null,
|
||||||
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
|
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
|
||||||
|
@@ -10,6 +10,7 @@ import { RequestService } from './request.service';
|
|||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { hasValueOperator } from '../../shared/empty.util';
|
import { hasValueOperator } from '../../shared/empty.util';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('RelationshipTypeDataService', () => {
|
describe('RelationshipTypeDataService', () => {
|
||||||
let service: RelationshipTypeDataService;
|
let service: RelationshipTypeDataService;
|
||||||
@@ -28,7 +29,7 @@ describe('RelationshipTypeDataService', () => {
|
|||||||
|
|
||||||
let buildList;
|
let buildList;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
let objectCache;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
restEndpointURL = 'https://rest.api/relationshiptypes';
|
restEndpointURL = 'https://rest.api/relationshiptypes';
|
||||||
@@ -60,21 +61,14 @@ describe('RelationshipTypeDataService', () => {
|
|||||||
|
|
||||||
buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2]));
|
buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2]));
|
||||||
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
|
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
|
||||||
objectCache = Object.assign({
|
objectCache = new ObjectCacheServiceStub();
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
remove: () => {
|
|
||||||
},
|
|
||||||
hasBySelfLinkObservable: () => observableOf(false)
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
}) as ObjectCacheService;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
return new RelationshipTypeDataService(
|
return new RelationshipTypeDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
144
src/app/core/data/update-data.service.spec.ts
Normal file
144
src/app/core/data/update-data.service.spec.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { HrefOnlyDataService } from './href-only-data.service';
|
||||||
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { Version } from '../shared/version.model';
|
||||||
|
import { VersionHistory } from '../shared/version-history.model';
|
||||||
|
import { RequestEntry } from './request-entry.model';
|
||||||
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
|
import { UpdateDataServiceImpl } from './update-data.service';
|
||||||
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
import { testCreateDataImplementation } from './base/create-data.spec';
|
||||||
|
import { testFindAllDataImplementation } from './base/find-all-data.spec';
|
||||||
|
import { testPutDataImplementation } from './base/put-data.spec';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
describe('VersionDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: UpdateDataServiceImpl<any>;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let hrefOnlyDataService: HrefOnlyDataService;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
id: '1234-1234',
|
||||||
|
uuid: '1234-1234',
|
||||||
|
bundles: observableOf({}),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith, Donald'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
|
id: '1',
|
||||||
|
draftVersion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockVersion: Version = Object.assign(new Version(), {
|
||||||
|
item: createSuccessfulRemoteDataObject$(item),
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
const mockVersionRD = createSuccessfulRemoteDataObject(mockVersion);
|
||||||
|
|
||||||
|
const endpointURL = `https://rest.api/rest/api/versioning/versions`;
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const comparatorEntry = {} as any;
|
||||||
|
function initTestService() {
|
||||||
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
|
return new UpdateDataServiceImpl(
|
||||||
|
'testLinkPath',
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
comparatorEntry,
|
||||||
|
10 * 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: hot('(a|)', {
|
||||||
|
a: mockVersionRD
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
spyOn((service as any), 'findById').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
service = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initService = () => new UpdateDataServiceImpl(null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
testPatchDataImplementation(initService);
|
||||||
|
testSearchDataImplementation(initService);
|
||||||
|
testDeleteDataImplementation(initService);
|
||||||
|
testCreateDataImplementation(initService);
|
||||||
|
testFindAllDataImplementation(initService);
|
||||||
|
testPutDataImplementation(initService);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,13 +1,317 @@
|
|||||||
import { Observable } from 'rxjs';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { AsyncSubject, from as observableFrom, Observable } from 'rxjs';
|
||||||
|
import {
|
||||||
|
find,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
toArray
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
import { PaginatedList } from './paginated-list.model';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import {
|
||||||
|
DeleteByIDRequest,
|
||||||
|
PostRequest
|
||||||
|
} from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||||
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
|
||||||
|
import { SearchData, SearchDataImpl } from './base/search-data';
|
||||||
|
import { CreateData, CreateDataImpl } from './base/create-data';
|
||||||
|
import { PatchData, PatchDataImpl } from './base/patch-data';
|
||||||
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import { PutData, PutDataImpl } from './base/put-data';
|
||||||
|
import { DeleteData, DeleteDataImpl } from './base/delete-data';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a data service to update a given object
|
* Interface to list the methods used by the injected service in components
|
||||||
*/
|
*/
|
||||||
export interface UpdateDataService<T> {
|
export interface UpdateDataService<T> {
|
||||||
patch(dso: T, operations: Operation[]): Observable<RemoteData<T>>;
|
patch(dso: T, operations: Operation[]): Observable<RemoteData<T>>;
|
||||||
update(object: T): Observable<RemoteData<T>>;
|
update(object: T): Observable<RemoteData<T>>;
|
||||||
commitUpdates(method?: RestRequestMethod);
|
commitUpdates(method?: RestRequestMethod): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific functionalities that not all services would need.
|
||||||
|
* Goal of the class is to update remote objects, handling custom methods that don't belong to BaseDataService
|
||||||
|
* The class implements also the following common interfaces
|
||||||
|
*
|
||||||
|
* findAllData: FindAllData<T>;
|
||||||
|
* searchData: SearchData<T>;
|
||||||
|
* createData: CreateData<T>;
|
||||||
|
* patchData: PatchData<T>;
|
||||||
|
* putData: PutData<T>;
|
||||||
|
* deleteData: DeleteData<T>;
|
||||||
|
*
|
||||||
|
* Custom methods are:
|
||||||
|
*
|
||||||
|
* deleteOnRelated - delete all related objects to the given one
|
||||||
|
* postOnRelated - post all the related objects to the given one
|
||||||
|
* invalidate - invalidate the DSpaceObject making all requests as stale
|
||||||
|
* invalidateByHref - invalidate the href making all requests as stale
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UpdateDataServiceImpl<T extends CacheableObject> extends IdentifiableDataService<T> implements FindAllData<T>, SearchData<T>, CreateData<T>, PatchData<T>, PutData<T>, DeleteData<T> {
|
||||||
|
private findAllData: FindAllDataImpl<T>;
|
||||||
|
private searchData: SearchDataImpl<T>;
|
||||||
|
private createData: CreateDataImpl<T>;
|
||||||
|
private patchData: PatchDataImpl<T>;
|
||||||
|
private putData: PutDataImpl<T>;
|
||||||
|
private deleteData: DeleteDataImpl<T>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected linkPath: string,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected comparator: ChangeAnalyzer<T>,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
) {
|
||||||
|
super(linkPath, requestService, rdbService, objectCache, halService, responseMsToLive);
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService ,this.responseMsToLive);
|
||||||
|
this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, comparator ,this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.putData = new PutDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService ,this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF with given options object
|
||||||
|
*
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linkPath The link path for the object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||||
|
return this.findAllData.getFindAllHref(options, linkPath, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object's search method with given options object
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||||
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options Find list options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a patch request for a specified object
|
||||||
|
* @param {T} object The object to send a patch request for
|
||||||
|
* @param {Operation[]} operations The patch operations to be performed
|
||||||
|
*/
|
||||||
|
patch(object: T, operations: Operation[]): Observable<RemoteData<T>> {
|
||||||
|
return this.patchData.patch(object, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
createPatchFromCache(object: T): Observable<Operation[]> {
|
||||||
|
return this.patchData.createPatchFromCache(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a PUT request for the specified object
|
||||||
|
*
|
||||||
|
* @param object The object to send a put request for.
|
||||||
|
*/
|
||||||
|
put(object: T): Observable<RemoteData<T>> {
|
||||||
|
return this.putData.put(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
|
* @param {DSpaceObject} object The given object
|
||||||
|
*/
|
||||||
|
update(object: T): Observable<RemoteData<T>> {
|
||||||
|
return this.patchData.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DSpaceObject on the server, and store the response
|
||||||
|
* in the object cache
|
||||||
|
*
|
||||||
|
* @param {CacheableObject} object
|
||||||
|
* The object to create
|
||||||
|
* @param {RequestParam[]} params
|
||||||
|
* Array with additional params to combine with query string
|
||||||
|
*/
|
||||||
|
create(object: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||||
|
return this.createData.create(object, ...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* Perform a post on an endpoint related item with ID. Ex.: endpoint/<itemId>/related?item=<relatedItemId>
|
||||||
|
* @param itemId The item id
|
||||||
|
* @param relatedItemId The related item Id
|
||||||
|
* @param body The optional POST body
|
||||||
|
* @return the RestResponse as an Observable
|
||||||
|
*/
|
||||||
|
public postOnRelated(itemId: string, relatedItemId: string, body?: any) {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const hrefObs = this.getIDHrefObs(itemId);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href + '/related?item=' + relatedItemId, body);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID<T>(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a delete on an endpoint related item. Ex.: endpoint/<itemId>/related
|
||||||
|
* @param itemId The item id
|
||||||
|
* @return the RestResponse as an Observable
|
||||||
|
*/
|
||||||
|
public deleteOnRelated(itemId: string): Observable<RemoteData<NoContent>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const hrefObs = this.getIDHrefObs(itemId);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new DeleteByIDRequest(requestId, href + '/related', itemId);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
|
||||||
|
* @param objectId The id of the object to be invalidated
|
||||||
|
* @return An Observable that will emit `true` once all requests are stale
|
||||||
|
*/
|
||||||
|
invalidate(objectId: string): Observable<boolean> {
|
||||||
|
return this.getIDHrefObs(objectId).pipe(
|
||||||
|
switchMap((href: string) => this.invalidateByHref(href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
|
||||||
|
* @param href The self link of the object to be invalidated
|
||||||
|
* @return An Observable that will emit `true` once all requests are stale
|
||||||
|
*/
|
||||||
|
invalidateByHref(href: string): Observable<boolean> {
|
||||||
|
const done$ = new AsyncSubject<boolean>();
|
||||||
|
|
||||||
|
this.objectCache.getByHref(href).pipe(
|
||||||
|
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
|
||||||
|
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
||||||
|
toArray(),
|
||||||
|
)),
|
||||||
|
).subscribe(() => {
|
||||||
|
done$.next(true);
|
||||||
|
done$.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
return done$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing DSpace Object on the server
|
||||||
|
* @param objectId The id of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
*/
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing DSpace Object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
commitUpdates(method?: RestRequestMethod) {
|
||||||
|
this.patchData.commitUpdates(method);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -128,7 +128,7 @@ describe('VersionDataService test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getHistoryFromVersion', () => {
|
describe('getHistoryFromVersion', () => {
|
||||||
it('should proxy the call to DataService.findByHref', () => {
|
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||||
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
|
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
|
@@ -315,7 +315,7 @@ describe('EPersonDataService', () => {
|
|||||||
service.deleteEPerson(EPersonMock).subscribe();
|
service.deleteEPerson(EPersonMock).subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call DataService.delete with the EPerson\'s UUID', () => {
|
it('should call UpdateDataServiceImpl.delete with the EPerson\'s UUID', () => {
|
||||||
expect(service.delete).toHaveBeenCalledWith(EPersonMock.id);
|
expect(service.delete).toHaveBeenCalledWith(EPersonMock.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -22,6 +22,7 @@ import {
|
|||||||
import { ReplaceOperation } from 'fast-json-patch';
|
import { ReplaceOperation } from 'fast-json-patch';
|
||||||
import { RequestEntry } from '../../../data/request-entry.model';
|
import { RequestEntry } from '../../../data/request-entry.model';
|
||||||
import { FindListOptions } from '../../../data/find-list-options.model';
|
import { FindListOptions } from '../../../data/find-list-options.model';
|
||||||
|
import { ObjectCacheServiceStub } from '../../../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('QualityAssuranceEventDataService', () => {
|
describe('QualityAssuranceEventDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -32,7 +33,7 @@ describe('QualityAssuranceEventDataService', () => {
|
|||||||
let responseCacheEntryC: RequestEntry;
|
let responseCacheEntryC: RequestEntry;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
let http: HttpClient;
|
let http: HttpClient;
|
||||||
@@ -91,7 +92,7 @@ describe('QualityAssuranceEventDataService', () => {
|
|||||||
buildFromRequestUUIDAndAwait: jasmine.createSpy('buildFromRequestUUIDAndAwait')
|
buildFromRequestUUIDAndAwait: jasmine.createSpy('buildFromRequestUUIDAndAwait')
|
||||||
});
|
});
|
||||||
|
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = new ObjectCacheServiceStub();
|
||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getEndpoint: cold('a|', { a: endpointURL })
|
getEndpoint: cold('a|', { a: endpointURL })
|
||||||
});
|
});
|
||||||
@@ -103,7 +104,7 @@ describe('QualityAssuranceEventDataService', () => {
|
|||||||
service = new QualityAssuranceEventDataService(
|
service = new QualityAssuranceEventDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
comparator
|
comparator
|
||||||
|
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '../../../../shared/mocks/notifications.mock';
|
} from '../../../../shared/mocks/notifications.mock';
|
||||||
import { RequestEntry } from '../../../data/request-entry.model';
|
import { RequestEntry } from '../../../data/request-entry.model';
|
||||||
import { QualityAssuranceSourceDataService } from './quality-assurance-source-data.service';
|
import { QualityAssuranceSourceDataService } from './quality-assurance-source-data.service';
|
||||||
|
import { ObjectCacheServiceStub } from '../../../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('QualityAssuranceSourceDataService', () => {
|
describe('QualityAssuranceSourceDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -26,7 +27,7 @@ describe('QualityAssuranceSourceDataService', () => {
|
|||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
let http: HttpClient;
|
let http: HttpClient;
|
||||||
@@ -63,7 +64,7 @@ describe('QualityAssuranceSourceDataService', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = new ObjectCacheServiceStub();
|
||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getEndpoint: cold('a|', { a: endpointURL })
|
getEndpoint: cold('a|', { a: endpointURL })
|
||||||
});
|
});
|
||||||
@@ -75,7 +76,7 @@ describe('QualityAssuranceSourceDataService', () => {
|
|||||||
service = new QualityAssuranceSourceDataService(
|
service = new QualityAssuranceSourceDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
notificationsService
|
notificationsService
|
||||||
);
|
);
|
||||||
|
@@ -19,6 +19,7 @@ import {
|
|||||||
qualityAssuranceTopicObjectMorePid
|
qualityAssuranceTopicObjectMorePid
|
||||||
} from '../../../../shared/mocks/notifications.mock';
|
} from '../../../../shared/mocks/notifications.mock';
|
||||||
import { RequestEntry } from '../../../data/request-entry.model';
|
import { RequestEntry } from '../../../data/request-entry.model';
|
||||||
|
import { ObjectCacheServiceStub } from '../../../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('QualityAssuranceTopicDataService', () => {
|
describe('QualityAssuranceTopicDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -26,7 +27,7 @@ describe('QualityAssuranceTopicDataService', () => {
|
|||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
let http: HttpClient;
|
let http: HttpClient;
|
||||||
@@ -63,7 +64,7 @@ describe('QualityAssuranceTopicDataService', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = new ObjectCacheServiceStub();
|
||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getEndpoint: cold('a|', { a: endpointURL })
|
getEndpoint: cold('a|', { a: endpointURL })
|
||||||
});
|
});
|
||||||
@@ -75,7 +76,7 @@ describe('QualityAssuranceTopicDataService', () => {
|
|||||||
service = new QualityAssuranceTopicDataService(
|
service = new QualityAssuranceTopicDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
notificationsService
|
notificationsService
|
||||||
);
|
);
|
||||||
|
@@ -20,13 +20,14 @@ import { FindListOptions } from '../data/find-list-options.model';
|
|||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../eperson/group-data.service';
|
import { GroupDataService } from '../eperson/group-data.service';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('ResourcePolicyService', () => {
|
describe('ResourcePolicyService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ResourcePolicyDataService;
|
let service: ResourcePolicyDataService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
let ePersonService: EPersonDataService;
|
let ePersonService: EPersonDataService;
|
||||||
@@ -139,14 +140,14 @@ describe('ResourcePolicyService', () => {
|
|||||||
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
|
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = new ObjectCacheServiceStub();
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
|
|
||||||
service = new ResourcePolicyDataService(
|
service = new ResourcePolicyDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
comparator,
|
comparator,
|
||||||
|
@@ -27,6 +27,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
|||||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||||
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
||||||
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
|
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
|
||||||
|
export const SCOPE: InjectionToken<string> = new InjectionToken<string>('scope');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all actions that have to do with search filters and facets
|
* Service that performs all actions that have to do with search filters and facets
|
||||||
|
@@ -4,7 +4,10 @@ import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-up
|
|||||||
* An interface to represent submission's upload section data.
|
* An interface to represent submission's upload section data.
|
||||||
*/
|
*/
|
||||||
export interface WorkspaceitemSectionUploadObject {
|
export interface WorkspaceitemSectionUploadObject {
|
||||||
|
/**
|
||||||
|
* Primary bitstream flag
|
||||||
|
*/
|
||||||
|
primary: string | null;
|
||||||
/**
|
/**
|
||||||
* A list of [[WorkspaceitemSectionUploadFileObject]]
|
* A list of [[WorkspaceitemSectionUploadFileObject]]
|
||||||
*/
|
*/
|
||||||
|
@@ -7,8 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
import { VocabularyDataService } from './vocabulary.data.service';
|
import { VocabularyDataService } from './vocabulary.data.service';
|
||||||
import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec';
|
import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec';
|
||||||
|
import { FindListOptions } from '../../data/find-list-options.model';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils';
|
||||||
|
|
||||||
describe('VocabularyDataService', () => {
|
describe('VocabularyDataService', () => {
|
||||||
|
let service: VocabularyDataService;
|
||||||
|
service = initTestService();
|
||||||
|
let restEndpointURL = 'https://rest.api/server/api/submission/vocabularies';
|
||||||
|
let vocabularyByMetadataAndCollectionEndpoint = `${restEndpointURL}/search/byMetadataAndCollection?metadata=dc.contributor.author&collection=1234-1234`;
|
||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
return new VocabularyDataService(null, null, null, null);
|
return new VocabularyDataService(null, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -17,4 +25,18 @@ describe('VocabularyDataService', () => {
|
|||||||
const initService = () => new VocabularyDataService(null, null, null, null);
|
const initService = () => new VocabularyDataService(null, null, null, null);
|
||||||
testFindAllDataImplementation(initService);
|
testFindAllDataImplementation(initService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getVocabularyByMetadataAndCollection', () => {
|
||||||
|
it('search vocabulary by metadata and collection calls expected methods', () => {
|
||||||
|
spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(vocabularyByMetadataAndCollectionEndpoint);
|
||||||
|
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null));
|
||||||
|
service.getVocabularyByMetadataAndCollection('dc.contributor.author', '1234-1234');
|
||||||
|
const options = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [Object.assign(new RequestParam('metadata', encodeURIComponent('dc.contributor.author'))),
|
||||||
|
Object.assign(new RequestParam('collection', encodeURIComponent('1234-1234')))]
|
||||||
|
});
|
||||||
|
expect((service as any).searchData.getSearchByHref).toHaveBeenCalledWith('byMetadataAndCollection', options);
|
||||||
|
expect(service.findByHref).toHaveBeenCalledWith(vocabularyByMetadataAndCollectionEndpoint, true, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -20,6 +20,8 @@ import { PaginatedList } from '../../data/paginated-list.model';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { VOCABULARY } from './models/vocabularies.resource-type';
|
import { VOCABULARY } from './models/vocabularies.resource-type';
|
||||||
import { dataService } from '../../data/base/data-service.decorator';
|
import { dataService } from '../../data/base/data-service.decorator';
|
||||||
|
import { SearchDataImpl } from '../../data/base/search-data';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data service to retrieve vocabularies from the REST server.
|
* Data service to retrieve vocabularies from the REST server.
|
||||||
@@ -27,7 +29,10 @@ import { dataService } from '../../data/base/data-service.decorator';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(VOCABULARY)
|
@dataService(VOCABULARY)
|
||||||
export class VocabularyDataService extends IdentifiableDataService<Vocabulary> implements FindAllData<Vocabulary> {
|
export class VocabularyDataService extends IdentifiableDataService<Vocabulary> implements FindAllData<Vocabulary> {
|
||||||
|
protected searchByMetadataAndCollectionPath = 'byMetadataAndCollection';
|
||||||
|
|
||||||
private findAllData: FindAllData<Vocabulary>;
|
private findAllData: FindAllData<Vocabulary>;
|
||||||
|
private searchData: SearchDataImpl<Vocabulary>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -38,6 +43,7 @@ export class VocabularyDataService extends IdentifiableDataService<Vocabulary> i
|
|||||||
super('vocabularies', requestService, rdbService, objectCache, halService);
|
super('vocabularies', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,4 +63,23 @@ export class VocabularyDataService extends IdentifiableDataService<Vocabulary> i
|
|||||||
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Vocabulary>[]): Observable<RemoteData<PaginatedList<Vocabulary>>> {
|
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<Vocabulary>[]): Observable<RemoteData<PaginatedList<Vocabulary>>> {
|
||||||
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the controlled vocabulary configured for the specified metadata and collection if any (/submission/vocabularies/search/{@link searchByMetadataAndCollectionPath}?metadata=<>&collection=<>)
|
||||||
|
* @param metadataField metadata field to search
|
||||||
|
* @param collectionUUID collection UUID where is configured the vocabulary
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getVocabularyByMetadataAndCollection(metadataField: string, collectionUUID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Vocabulary>[]): Observable<RemoteData<Vocabulary>> {
|
||||||
|
const findListOptions = new FindListOptions();
|
||||||
|
findListOptions.searchParams = [new RequestParam('metadata', encodeURIComponent(metadataField)),
|
||||||
|
new RequestParam('collection', encodeURIComponent(collectionUUID))];
|
||||||
|
const href$ = this.searchData.getSearchByHref(this.searchByMetadataAndCollectionPath, findListOptions, ...linksToFollow);
|
||||||
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
|||||||
import { RequestEntry } from '../../data/request-entry.model';
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
import { VocabularyDataService } from './vocabulary.data.service';
|
import { VocabularyDataService } from './vocabulary.data.service';
|
||||||
import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service';
|
import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service';
|
||||||
|
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('VocabularyService', () => {
|
describe('VocabularyService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -205,6 +206,7 @@ describe('VocabularyService', () => {
|
|||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
|
objectCache = new ObjectCacheServiceStub() as ObjectCacheService;
|
||||||
|
|
||||||
return new VocabularyService(
|
return new VocabularyService(
|
||||||
requestService,
|
requestService,
|
||||||
@@ -253,7 +255,9 @@ describe('VocabularyService', () => {
|
|||||||
spyOn((service as any).vocabularyDataService, 'findById').and.callThrough();
|
spyOn((service as any).vocabularyDataService, 'findById').and.callThrough();
|
||||||
spyOn((service as any).vocabularyDataService, 'findAll').and.callThrough();
|
spyOn((service as any).vocabularyDataService, 'findAll').and.callThrough();
|
||||||
spyOn((service as any).vocabularyDataService, 'findByHref').and.callThrough();
|
spyOn((service as any).vocabularyDataService, 'findByHref').and.callThrough();
|
||||||
|
spyOn((service as any).vocabularyDataService, 'getVocabularyByMetadataAndCollection').and.callThrough();
|
||||||
spyOn((service as any).vocabularyDataService.findAllData, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL));
|
spyOn((service as any).vocabularyDataService.findAllData, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL));
|
||||||
|
spyOn((service as any).vocabularyDataService.searchData, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -310,6 +314,23 @@ describe('VocabularyService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getVocabularyByMetadataAndCollection', () => {
|
||||||
|
it('should proxy the call to vocabularyDataService.getVocabularyByMetadataAndCollection', () => {
|
||||||
|
scheduler.schedule(() => service.getVocabularyByMetadataAndCollection(metadata, collectionUUID));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).vocabularyDataService.getVocabularyByMetadataAndCollection).toHaveBeenCalledWith(metadata, collectionUUID, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<Vocabulary> for the object with the given metadata and collection', () => {
|
||||||
|
const result = service.getVocabularyByMetadataAndCollection(metadata, collectionUUID);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: vocabularyRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('vocabulary entries', () => {
|
describe('vocabulary entries', () => {
|
||||||
|
@@ -87,6 +87,23 @@ export class VocabularyService {
|
|||||||
return this.vocabularyDataService.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.vocabularyDataService.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the controlled vocabulary configured for the specified metadata and collection if any
|
||||||
|
* @param metadataField metadata field to search
|
||||||
|
* @param collectionUUID collection UUID where is configured the vocabulary
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<Vocabulary>>}
|
||||||
|
* Return an observable that emits vocabulary object
|
||||||
|
*/
|
||||||
|
getVocabularyByMetadataAndCollection(metadataField: string, collectionUUID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Vocabulary>[]): Observable<RemoteData<Vocabulary>> {
|
||||||
|
return this.vocabularyDataService.getVocabularyByMetadataAndCollection(metadataField, collectionUUID, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the {@link VocabularyEntry} list for a given {@link Vocabulary}
|
* Return the {@link VocabularyEntry} list for a given {@link Vocabulary}
|
||||||
*
|
*
|
||||||
|
@@ -126,7 +126,7 @@ describe('WorkflowItemDataService test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findByItem', () => {
|
describe('findByItem', () => {
|
||||||
it('should proxy the call to DataService.findByHref', () => {
|
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||||
@@ -21,6 +21,11 @@ import { RequestEntry } from '../data/request-entry.model';
|
|||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { testSearchDataImplementation } from '../data/base/search-data.spec';
|
import { testSearchDataImplementation } from '../data/base/search-data.spec';
|
||||||
import { testDeleteDataImplementation } from '../data/base/delete-data.spec';
|
import { testDeleteDataImplementation } from '../data/base/delete-data.spec';
|
||||||
|
import { SearchData } from '../data/base/search-data';
|
||||||
|
import { DeleteData } from '../data/base/delete-data';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { PostRequest } from '../data/request.models';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
|
||||||
describe('WorkspaceitemDataService test', () => {
|
describe('WorkspaceitemDataService test', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -68,15 +73,12 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||||
|
|
||||||
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||||
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
|
||||||
const searchRequestURL$ = observableOf(searchRequestURL);
|
|
||||||
|
|
||||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = {} as ObjectCacheService;
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = {} as any;
|
|
||||||
const comparatorEntry = {} as any;
|
const comparatorEntry = {} as any;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const pageInfo = new PageInfo();
|
const pageInfo = new PageInfo();
|
||||||
@@ -84,18 +86,23 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
function initTestService() {
|
function initTestService() {
|
||||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
return new WorkspaceitemDataService(
|
return new WorkspaceitemDataService(
|
||||||
|
comparatorEntry,
|
||||||
|
halService,
|
||||||
|
http,
|
||||||
|
notificationsService,
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache,
|
||||||
halService,
|
store
|
||||||
notificationsService,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('composition', () => {
|
describe('composition', () => {
|
||||||
const initService = () => new WorkspaceitemDataService(null, null, null, null, null);
|
const initSearchService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as SearchData<any>;
|
||||||
testSearchDataImplementation(initService);
|
const initDeleteService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as DeleteData<any>;
|
||||||
testDeleteDataImplementation(initService);
|
|
||||||
|
testSearchDataImplementation(initSearchService);
|
||||||
|
testDeleteDataImplementation(initDeleteService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -104,7 +111,7 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getEndpoint: cold('a', { a: endpointURL })
|
getEndpoint: observableOf(endpointURL)
|
||||||
});
|
});
|
||||||
responseCacheEntry = new RequestEntry();
|
responseCacheEntry = new RequestEntry();
|
||||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
@@ -120,13 +127,13 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildSingle: hot('a|', {
|
buildSingle: hot('a|', {
|
||||||
a: wsiRD
|
a: wsiRD
|
||||||
})
|
}),
|
||||||
|
buildFromRequestUUID: createSuccessfulRemoteDataObject$({})
|
||||||
});
|
});
|
||||||
|
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
spyOn((service as any), 'findByHref').and.callThrough();
|
spyOn((service as any), 'findByHref').and.callThrough();
|
||||||
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -134,11 +141,11 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findByItem', () => {
|
describe('findByItem', () => {
|
||||||
it('should proxy the call to DataService.findByHref', () => {
|
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
const searchUrl = service.getIDHref('item', [new RequestParam('uuid', encodeURIComponent('1234-1234'))]);
|
||||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
expect((service as any).findByHref).toHaveBeenCalledWith(searchUrl, true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||||
@@ -150,6 +157,19 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('importExternalSourceEntry', () => {
|
||||||
|
it('should send a POST request containing the provided item request', (done) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
service.importExternalSourceEntry('externalHref', 'testId').subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestUUID, `${endpointURL}?owningCollection=testId`, 'externalHref', options));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,46 +1,58 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import {HttpOptions} from '../dspace-rest/dspace-rest.service';
|
||||||
|
import {find, map} from 'rxjs/operators';
|
||||||
|
import {PostRequest} from '../data/request.models';
|
||||||
|
import {hasValue} from '../../shared/empty.util';
|
||||||
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||||
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
||||||
import { PaginatedList } from '../data/paginated-list.model';
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
|
||||||
import { dataService } from '../data/base/data-service.decorator';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(WorkspaceItem.type)
|
@dataService(WorkspaceItem.type)
|
||||||
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements SearchData<WorkspaceItem>, DeleteData<WorkspaceItem> {
|
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements DeleteData<WorkspaceItem>, SearchData<WorkspaceItem>{
|
||||||
|
protected linkPath = 'workspaceitems';
|
||||||
protected searchByItemLinkPath = 'item';
|
protected searchByItemLinkPath = 'item';
|
||||||
|
private deleteData: DeleteData<WorkspaceItem>;
|
||||||
private searchData: SearchDataImpl<WorkspaceItem>;
|
private searchData: SearchData<WorkspaceItem>;
|
||||||
private deleteData: DeleteDataImpl<WorkspaceItem>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected store: Store<CoreState>) {
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
) {
|
|
||||||
super('workspaceitems', requestService, rdbService, objectCache, halService);
|
super('workspaceitems', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
|
||||||
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
}
|
||||||
|
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the WorkspaceItem object found through the UUID of an item
|
* Return the WorkspaceItem object found through the UUID of an item
|
||||||
*
|
*
|
||||||
@@ -55,21 +67,46 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
|
|||||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||||
const findListOptions = new FindListOptions();
|
const findListOptions = new FindListOptions();
|
||||||
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||||
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
const href$ = this.getIDHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the HREF for a specific object's search method with given options object
|
* Import an external source entry into a collection
|
||||||
*
|
* @param externalSourceEntryHref
|
||||||
* @param searchMethod The search method for the object
|
* @param collectionId
|
||||||
* @param options The [[FindListOptions]] object
|
|
||||||
* @return {Observable<string>}
|
|
||||||
* Return an observable that emits created HREF
|
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
|
||||||
*/
|
*/
|
||||||
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow): Observable<string> {
|
public importExternalSourceEntry(externalSourceEntryHref: string, collectionId: string): Observable<RemoteData<WorkspaceItem>> {
|
||||||
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`));
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, externalSourceEntryHref, options);
|
||||||
|
this.requestService.send(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing object on the server
|
||||||
|
* @param href The self link of the object to be removed
|
||||||
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
|
* metadata should be saved as real metadata
|
||||||
|
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
||||||
|
* errorMessage, timeCompleted, etc
|
||||||
|
* Only emits once all request related to the DSO has been invalidated.
|
||||||
|
*/
|
||||||
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,33 +123,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
|
|||||||
* @return {Observable<RemoteData<PaginatedList<T>>}
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
* Return an observable that emits response from the server
|
* Return an observable that emits response from the server
|
||||||
*/
|
*/
|
||||||
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
|
||||||
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing object on the server
|
|
||||||
* @param objectId The id of the object to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
|
||||||
* errorMessage, timeCompleted, etc
|
|
||||||
*/
|
|
||||||
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
|
||||||
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing object on the server
|
|
||||||
* @param href The self link of the object to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode,
|
|
||||||
* errorMessage, timeCompleted, etc
|
|
||||||
* Only emits once all request related to the DSO has been invalidated.
|
|
||||||
*/
|
|
||||||
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
|
||||||
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for the Suggestion object
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SUGGESTION = new ResourceType('suggestion');
|
@@ -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');
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { SUGGESTION_SOURCE } from './suggestion-source-object.resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import {CacheableObject} from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface representing the Suggestion Source model
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class SuggestionSource implements CacheableObject {
|
||||||
|
/**
|
||||||
|
* A string representing the kind of object, e.g. community, item, …
|
||||||
|
*/
|
||||||
|
static type = SUGGESTION_SOURCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion Target id
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of suggestions provided by Suggestion Target for
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this ConfigObject
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links to all related resources returned by the rest api.
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
suggestiontargets: HALLink
|
||||||
|
};
|
||||||
|
}
|
@@ -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');
|
@@ -0,0 +1,61 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
import { SUGGESTION_TARGET } from './suggestion-target-object.resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface representing the Suggestion Target model
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class SuggestionTarget implements CacheableObject {
|
||||||
|
/**
|
||||||
|
* A string representing the kind of object, e.g. community, item, …
|
||||||
|
*/
|
||||||
|
static type = SUGGESTION_TARGET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion Target id
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion Target name to display
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
display: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion Target source to display
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
source: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of suggestions provided by Suggestion Target for
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this ConfigObject
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links to all related resources returned by the rest api.
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
suggestions: HALLink,
|
||||||
|
target: HALLink
|
||||||
|
};
|
||||||
|
}
|
@@ -0,0 +1,88 @@
|
|||||||
|
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { SUGGESTION } from './suggestion-objects.resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models';
|
||||||
|
import {CacheableObject} from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface representing Suggestion Evidences such as scores (authorScore, datescore)
|
||||||
|
*/
|
||||||
|
export interface SuggestionEvidences {
|
||||||
|
[sectionId: string]: {
|
||||||
|
score: string;
|
||||||
|
notes: string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The interface representing the Suggestion Source model
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class Suggestion implements CacheableObject {
|
||||||
|
/**
|
||||||
|
* A string representing the kind of object, e.g. community, item, …
|
||||||
|
*/
|
||||||
|
static type = SUGGESTION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion id
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion name to display
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
display: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion source to display
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
source: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Suggestion external source uri
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
externalSourceUri: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Total Score of the suggestion
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
score: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of suggestions provided by Suggestion Target for
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
evidences: SuggestionEvidences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All metadata of this suggestion object
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserializeAs(MetadataMapSerializer)
|
||||||
|
metadata: MetadataMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this ConfigObject
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links to all related resources returned by the rest api.
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
target: HALLink
|
||||||
|
};
|
||||||
|
}
|
@@ -0,0 +1,96 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../../data/base/data-service.decorator';
|
||||||
|
import { SUGGESTION_SOURCE } from '../models/suggestion-source-object.resource-type';
|
||||||
|
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
|
||||||
|
import { SuggestionSource } from '../models/suggestion-source.model';
|
||||||
|
import { FindAllData, FindAllDataImpl } from '../../data/base/find-all-data';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { CoreState } from '../../core-state.model';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { FindListOptions } from '../../data/find-list-options.model';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { PaginatedList } from '../../data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that retrieves Suggestion Source data
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SUGGESTION_SOURCE)
|
||||||
|
export class SuggestionSourceDataService extends IdentifiableDataService<SuggestionSource> {
|
||||||
|
|
||||||
|
protected linkPath = 'suggestionsources';
|
||||||
|
|
||||||
|
private findAllData: FindAllData<SuggestionSource>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<SuggestionSource>) {
|
||||||
|
super('suggestionsources', requestService, rdbService, objectCache, halService);
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion source.
|
||||||
|
*
|
||||||
|
* @param options Find list options object.
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
*
|
||||||
|
* @return Observable<RemoteData<PaginatedList<QualityAssuranceSourceObject>>>
|
||||||
|
* The list of Quality Assurance source.
|
||||||
|
*/
|
||||||
|
public getSources(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SuggestionSource>[]): Observable<RemoteData<PaginatedList<SuggestionSource>>> {
|
||||||
|
return this.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a single Suggestoin source.
|
||||||
|
*
|
||||||
|
* @param id The Quality Assurance source id
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
*
|
||||||
|
* @return Observable<RemoteData<QualityAssuranceSourceObject>> The Quality Assurance source.
|
||||||
|
*/
|
||||||
|
public getSource(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SuggestionSource>[]): Observable<RemoteData<SuggestionSource>> {
|
||||||
|
return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options Find list options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SuggestionSource>[]): Observable<RemoteData<PaginatedList<SuggestionSource>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,115 @@
|
|||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { RestResponse } from '../../cache/response.models';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core-state.model';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||||
|
import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec';
|
||||||
|
import { FindAllData } from '../../data/base/find-all-data';
|
||||||
|
import { GetRequest } from '../../data/request.models';
|
||||||
|
import {
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../../shared/remote-data.utils';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../data/request-entry-state.model';
|
||||||
|
import { SuggestionSourceDataService } from './suggestion-source-data.service';
|
||||||
|
import { SuggestionSource } from '../models/suggestion-source.model';
|
||||||
|
|
||||||
|
describe('SuggestionSourceDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: SuggestionSourceDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let http: HttpClient;
|
||||||
|
let comparator: DefaultChangeAnalyzer<SuggestionSource>;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const endpointURL = `https://rest.api/rest/api/suggestionsources`;
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
const remoteDataMocks = {
|
||||||
|
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new SuggestionSourceDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
http = {} as HttpClient;
|
||||||
|
notificationsService = {} as NotificationsService;
|
||||||
|
comparator = {} as DefaultChangeAnalyzer<SuggestionSource>;
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf(endpointURL)
|
||||||
|
});
|
||||||
|
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||||
|
buildList: cold('a', { a: remoteDataMocks.Success })
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initFindAllService = () => new SuggestionSourceDataService(null, null, null, null, null, null, null, null) as unknown as FindAllData<any>;
|
||||||
|
testFindAllDataImplementation(initFindAllService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSources', () => {
|
||||||
|
it('should send a new GetRequest', () => {
|
||||||
|
const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}`);
|
||||||
|
scheduler.schedule(() => service.getSources().subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSource', () => {
|
||||||
|
it('should send a new GetRequest', () => {
|
||||||
|
const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/testId`);
|
||||||
|
scheduler.schedule(() => service.getSource('testId').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,173 @@
|
|||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { SuggestionDataServiceImpl, SuggestionsDataService } from './suggestions-data.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { Suggestion } from './models/suggestion.model';
|
||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { RequestEntry } from '../data/request-entry.model';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RequestEntryState } from '../data/request-entry-state.model';
|
||||||
|
import { SuggestionSource } from './models/suggestion-source.model';
|
||||||
|
import { SuggestionTarget } from './models/suggestion-target.model';
|
||||||
|
import { SuggestionSourceDataService } from './source/suggestion-source-data.service';
|
||||||
|
import { SuggestionTargetDataService } from './target/suggestion-target-data.service';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
|
describe('SuggestionDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: SuggestionsDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let http: HttpClient;
|
||||||
|
let comparatorSuggestion: DefaultChangeAnalyzer<Suggestion>;
|
||||||
|
let comparatorSuggestionSource: DefaultChangeAnalyzer<SuggestionSource>;
|
||||||
|
let comparatorSuggestionTarget: DefaultChangeAnalyzer<SuggestionTarget>;
|
||||||
|
let suggestionSourcesDataService: SuggestionSourceDataService;
|
||||||
|
let suggestionTargetsDataService: SuggestionTargetDataService;
|
||||||
|
let suggestionsDataService: SuggestionDataServiceImpl;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
|
||||||
|
const testSource = 'test-source';
|
||||||
|
const testUserId = '1234-4321';
|
||||||
|
const endpointURL = `https://rest.api/rest/api/`;
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
const remoteDataMocks = {
|
||||||
|
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new SuggestionsDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparatorSuggestion,
|
||||||
|
comparatorSuggestionSource,
|
||||||
|
comparatorSuggestionTarget
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
http = {} as HttpClient;
|
||||||
|
notificationsService = {} as NotificationsService;
|
||||||
|
comparatorSuggestion = {} as DefaultChangeAnalyzer<Suggestion>;
|
||||||
|
comparatorSuggestionTarget = {} as DefaultChangeAnalyzer<SuggestionTarget>;
|
||||||
|
comparatorSuggestionSource = {} as DefaultChangeAnalyzer<SuggestionSource>;
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
setStaleByHrefSubstring: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf(endpointURL)
|
||||||
|
});
|
||||||
|
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||||
|
buildList: cold('a', { a: remoteDataMocks.Success })
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
suggestionSourcesDataService = jasmine.createSpyObj('suggestionSourcesDataService', {
|
||||||
|
getSources: observableOf(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestionTargetsDataService = jasmine.createSpyObj('suggestionTargetsDataService', {
|
||||||
|
getTargets: observableOf(null),
|
||||||
|
getTargetsByUser: observableOf(null),
|
||||||
|
findById: observableOf(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestionsDataService = jasmine.createSpyObj('suggestionsDataService', {
|
||||||
|
searchBy: observableOf(null),
|
||||||
|
delete: observableOf(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/dot-notation */
|
||||||
|
service['suggestionSourcesDataService'] = suggestionSourcesDataService;
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/dot-notation */
|
||||||
|
service['suggestionTargetsDataService'] = suggestionTargetsDataService;
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/dot-notation */
|
||||||
|
service['suggestionsDataService'] = suggestionsDataService;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Suggestion targets service', () => {
|
||||||
|
it('should call suggestionSourcesDataService.getTargets', () => {
|
||||||
|
const options = {
|
||||||
|
searchParams: [new RequestParam('source', testSource)]
|
||||||
|
};
|
||||||
|
service.getTargets(testSource);
|
||||||
|
expect(suggestionTargetsDataService.getTargets).toHaveBeenCalledWith('findBySource', options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call suggestionSourcesDataService.getTargetsByUser', () => {
|
||||||
|
const options = {
|
||||||
|
searchParams: [new RequestParam('target', testUserId)]
|
||||||
|
};
|
||||||
|
service.getTargetsByUser(testUserId);
|
||||||
|
expect(suggestionTargetsDataService.getTargetsByUser).toHaveBeenCalledWith(testUserId, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call suggestionSourcesDataService.getTargetById', () => {
|
||||||
|
service.getTargetById('1');
|
||||||
|
expect(suggestionTargetsDataService.findById).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Suggestion sources service', () => {
|
||||||
|
it('should call suggestionSourcesDataService.getSources', () => {
|
||||||
|
service.getSources();
|
||||||
|
expect(suggestionSourcesDataService.getSources).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Suggestion service', () => {
|
||||||
|
it('should call suggestionsDataService.searchBy', () => {
|
||||||
|
const options = {
|
||||||
|
searchParams: [new RequestParam('target', testUserId), new RequestParam('source', testSource)]
|
||||||
|
};
|
||||||
|
service.getSuggestionsByTargetAndSource(testUserId, testSource);
|
||||||
|
expect(suggestionsDataService.searchBy).toHaveBeenCalledWith('findByTargetAndSource', options, false, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call suggestionsDataService.delete', () => {
|
||||||
|
service.deleteSuggestion('1');
|
||||||
|
expect(suggestionsDataService.delete).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request service', () => {
|
||||||
|
it('should call requestService.setStaleByHrefSubstring', () => {
|
||||||
|
service.clearSuggestionRequests();
|
||||||
|
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,229 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { UpdateDataServiceImpl } from '../data/update-data.service';
|
||||||
|
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { SUGGESTION } from './models/suggestion-objects.resource-type';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
import { SuggestionSource } from './models/suggestion-source.model';
|
||||||
|
import { SuggestionTarget } from './models/suggestion-target.model';
|
||||||
|
import { Suggestion } from './models/suggestion.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import {CoreState} from '../core-state.model';
|
||||||
|
import {FindListOptions} from '../data/find-list-options.model';
|
||||||
|
import { SuggestionSourceDataService } from './source/suggestion-source-data.service';
|
||||||
|
import { SuggestionTargetDataService } from './target/suggestion-target-data.service';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private UpdateDataServiceImpl implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
export class SuggestionDataServiceImpl extends UpdateDataServiceImpl<Suggestion> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize service variables
|
||||||
|
* @param {RequestService} requestService
|
||||||
|
* @param {RemoteDataBuildService} rdbService
|
||||||
|
* @param {Store<CoreState>} store
|
||||||
|
* @param {ObjectCacheService} objectCache
|
||||||
|
* @param {HALEndpointService} halService
|
||||||
|
* @param {NotificationsService} notificationsService
|
||||||
|
* @param {HttpClient} http
|
||||||
|
* @param {ChangeAnalyzer<Suggestion>} comparator
|
||||||
|
* @param responseMsToLive
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: ChangeAnalyzer<Suggestion>,
|
||||||
|
protected responseMsToLive: number,
|
||||||
|
) {
|
||||||
|
super('suggestions', requestService, rdbService, objectCache, halService, notificationsService, comparator ,responseMsToLive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service handling all Suggestion Target REST requests.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SUGGESTION)
|
||||||
|
export class SuggestionsDataService {
|
||||||
|
protected searchFindBySourceMethod = 'findBySource';
|
||||||
|
protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private UpdateDataServiceImpl implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
private suggestionsDataService: SuggestionDataServiceImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private UpdateDataServiceImpl implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
private suggestionSourcesDataService: SuggestionSourceDataService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private UpdateDataServiceImpl implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
private suggestionTargetsDataService: SuggestionTargetDataService;
|
||||||
|
|
||||||
|
private responseMsToLive = 10 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize service variables
|
||||||
|
* @param {RequestService} requestService
|
||||||
|
* @param {RemoteDataBuildService} rdbService
|
||||||
|
* @param {ObjectCacheService} objectCache
|
||||||
|
* @param {HALEndpointService} halService
|
||||||
|
* @param {NotificationsService} notificationsService
|
||||||
|
* @param {HttpClient} http
|
||||||
|
* @param {DefaultChangeAnalyzer<Suggestion>} comparatorSuggestions
|
||||||
|
* @param {DefaultChangeAnalyzer<SuggestionSource>} comparatorSources
|
||||||
|
* @param {DefaultChangeAnalyzer<SuggestionTarget>} comparatorTargets
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparatorSuggestions: DefaultChangeAnalyzer<Suggestion>,
|
||||||
|
protected comparatorSources: DefaultChangeAnalyzer<SuggestionSource>,
|
||||||
|
protected comparatorTargets: DefaultChangeAnalyzer<SuggestionTarget>,
|
||||||
|
) {
|
||||||
|
this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions, this.responseMsToLive);
|
||||||
|
this.suggestionSourcesDataService = new SuggestionSourceDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources);
|
||||||
|
this.suggestionTargetsDataService = new SuggestionTargetDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion Sources
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<SuggestionSource>>>
|
||||||
|
* The list of Suggestion Sources.
|
||||||
|
*/
|
||||||
|
public getSources(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<SuggestionSource>>> {
|
||||||
|
return this.suggestionSourcesDataService.getSources(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion Target for a given source
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* The source for which to find targets.
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @param linksToFollow
|
||||||
|
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<SuggestionTarget>>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargets(
|
||||||
|
source: string,
|
||||||
|
options: FindListOptions = {},
|
||||||
|
...linksToFollow: FollowLinkConfig<SuggestionTarget>[]
|
||||||
|
): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
options.searchParams = [new RequestParam('source', source)];
|
||||||
|
|
||||||
|
return this.suggestionTargetsDataService.getTargets(this.searchFindBySourceMethod, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion Target for a given user
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* The user Id for which to find targets.
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @param linksToFollow
|
||||||
|
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<SuggestionTarget>>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargetsByUser(
|
||||||
|
userId: string,
|
||||||
|
options: FindListOptions = {},
|
||||||
|
...linksToFollow: FollowLinkConfig<SuggestionTarget>[]
|
||||||
|
): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
options.searchParams = [new RequestParam('target', userId)];
|
||||||
|
return this.suggestionTargetsDataService.getTargetsByUser(userId, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a Suggestion Target for a given id
|
||||||
|
*
|
||||||
|
* @param targetId
|
||||||
|
* The target id to retrieve.
|
||||||
|
*
|
||||||
|
* @return Observable<RemoteData<SuggestionTarget>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargetById(targetId: string): Observable<RemoteData<SuggestionTarget>> {
|
||||||
|
return this.suggestionTargetsDataService.findById(targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to delete Suggestion
|
||||||
|
* @suggestionId
|
||||||
|
*/
|
||||||
|
public deleteSuggestion(suggestionId: string): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.suggestionsDataService.delete(suggestionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion for a given target and source
|
||||||
|
*
|
||||||
|
* @param target
|
||||||
|
* The target for which to find suggestions.
|
||||||
|
* @param source
|
||||||
|
* The source for which to find suggestions.
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @param linksToFollow
|
||||||
|
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<Suggestion>>>
|
||||||
|
* The list of Suggestion.
|
||||||
|
*/
|
||||||
|
public getSuggestionsByTargetAndSource(
|
||||||
|
target: string,
|
||||||
|
source: string,
|
||||||
|
options: FindListOptions = {},
|
||||||
|
...linksToFollow: FollowLinkConfig<Suggestion>[]
|
||||||
|
): Observable<RemoteData<PaginatedList<Suggestion>>> {
|
||||||
|
options.searchParams = [
|
||||||
|
new RequestParam('target', target),
|
||||||
|
new RequestParam('source', source)
|
||||||
|
];
|
||||||
|
|
||||||
|
return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, false, true, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear findByTargetAndSource suggestions requests from cache
|
||||||
|
*/
|
||||||
|
public clearSuggestionRequests() {
|
||||||
|
this.requestService.setStaleByHrefSubstring(this.searchFindByTargetAndSourceMethod);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,141 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../../data/base/data-service.decorator';
|
||||||
|
|
||||||
|
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
|
||||||
|
import { SuggestionTarget } from '../models/suggestion-target.model';
|
||||||
|
import { FindAllData, FindAllDataImpl } from '../../data/base/find-all-data';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { CoreState } from '../../core-state.model';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { FindListOptions } from '../../data/find-list-options.model';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { PaginatedList } from '../../data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { SearchData, SearchDataImpl } from '../../data/base/search-data';
|
||||||
|
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||||
|
import { SUGGESTION_TARGET } from '../models/suggestion-target-object.resource-type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SUGGESTION_TARGET)
|
||||||
|
export class SuggestionTargetDataService extends IdentifiableDataService<SuggestionTarget> {
|
||||||
|
|
||||||
|
protected linkPath = 'suggestiontargets';
|
||||||
|
private findAllData: FindAllData<SuggestionTarget>;
|
||||||
|
private searchData: SearchData<SuggestionTarget>;
|
||||||
|
protected searchFindBySourceMethod = 'findBySource';
|
||||||
|
protected searchFindByTargetMethod = 'findByTarget';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<SuggestionTarget>) {
|
||||||
|
super('suggestiontargets', requestService, rdbService, objectCache, halService);
|
||||||
|
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion Target for a given source
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* The source for which to find targets.
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @param linksToFollow
|
||||||
|
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<SuggestionTarget>>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargets(
|
||||||
|
source: string,
|
||||||
|
options: FindListOptions = {},
|
||||||
|
...linksToFollow: FollowLinkConfig<SuggestionTarget>[]
|
||||||
|
): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
options.searchParams = [new RequestParam('source', source)];
|
||||||
|
|
||||||
|
return this.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of Suggestion Target for a given user
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* The user Id for which to find targets.
|
||||||
|
* @param options
|
||||||
|
* Find list options object.
|
||||||
|
* @param linksToFollow
|
||||||
|
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||||
|
* @return Observable<RemoteData<PaginatedList<SuggestionTarget>>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargetsByUser(
|
||||||
|
userId: string,
|
||||||
|
options: FindListOptions = {},
|
||||||
|
...linksToFollow: FollowLinkConfig<SuggestionTarget>[]
|
||||||
|
): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
options.searchParams = [new RequestParam('target', userId)];
|
||||||
|
|
||||||
|
return this.searchBy(this.searchFindByTargetMethod, options, true, true, ...linksToFollow);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return a Suggestion Target for a given id
|
||||||
|
*
|
||||||
|
* @param targetId
|
||||||
|
* The target id to retrieve.
|
||||||
|
*
|
||||||
|
* @return Observable<RemoteData<SuggestionTarget>>
|
||||||
|
* The list of Suggestion Target.
|
||||||
|
*/
|
||||||
|
public getTargetById(targetId: string): Observable<RemoteData<SuggestionTarget>> {
|
||||||
|
return this.findById(targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SuggestionTarget>[]): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options Find list options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
|
*/
|
||||||
|
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SuggestionTarget>[]): Observable<RemoteData<PaginatedList<SuggestionTarget>>> {
|
||||||
|
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,138 @@
|
|||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { RequestEntry } from '../../data/request-entry.model';
|
||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { RestResponse } from '../../cache/response.models';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core-state.model';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { SearchData } from '../../data/base/search-data';
|
||||||
|
import { testSearchDataImplementation } from '../../data/base/search-data.spec';
|
||||||
|
import { SuggestionTargetDataService } from './suggestion-target-data.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||||
|
import { SuggestionTarget } from '../models/suggestion-target.model';
|
||||||
|
import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec';
|
||||||
|
import { FindAllData } from '../../data/base/find-all-data';
|
||||||
|
import { GetRequest } from '../../data/request.models';
|
||||||
|
import {
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../../shared/remote-data.utils';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../data/request-entry-state.model';
|
||||||
|
|
||||||
|
describe('SuggestionTargetDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: SuggestionTargetDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let http: HttpClient;
|
||||||
|
let comparator: DefaultChangeAnalyzer<SuggestionTarget>;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const endpointURL = `https://rest.api/rest/api/suggestiontargets`;
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
const remoteDataMocks = {
|
||||||
|
Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new SuggestionTargetDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
http = {} as HttpClient;
|
||||||
|
notificationsService = {} as NotificationsService;
|
||||||
|
comparator = {} as DefaultChangeAnalyzer<SuggestionTarget>;
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf(endpointURL)
|
||||||
|
});
|
||||||
|
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$({}, 500),
|
||||||
|
buildList: cold('a', { a: remoteDataMocks.Success })
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composition', () => {
|
||||||
|
const initSearchService = () => new SuggestionTargetDataService(null, null, null, null, null, null, null, null) as unknown as SearchData<any>;
|
||||||
|
const initFindAllService = () => new SuggestionTargetDataService(null, null, null, null, null, null, null, null) as unknown as FindAllData<any>;
|
||||||
|
testSearchDataImplementation(initSearchService);
|
||||||
|
testFindAllDataImplementation(initFindAllService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTargetById', () => {
|
||||||
|
it('should send a new GetRequest', () => {
|
||||||
|
const expected = new GetRequest(requestService.generateRequestId(), endpointURL + '/testId');
|
||||||
|
scheduler.schedule(() => service.getTargetById('testId').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTargetsByUser', () => {
|
||||||
|
it('should send a new GetRequest', () => {
|
||||||
|
const options = {
|
||||||
|
searchParams: [new RequestParam('target', 'testId')]
|
||||||
|
};
|
||||||
|
const searchFindByTargetMethod = 'findByTarget';
|
||||||
|
const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/search/${searchFindByTargetMethod}?target=testId`);
|
||||||
|
scheduler.schedule(() => service.getTargetsByUser('testId', options).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTargets', () => {
|
||||||
|
it('should send a new GetRequest', () => {
|
||||||
|
const options = {
|
||||||
|
searchParams: [new RequestParam('source', 'testId')]
|
||||||
|
};
|
||||||
|
const searchFindBySourceMethod = 'findBySource';
|
||||||
|
const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/search/${searchFindBySourceMethod}?source=testId`);
|
||||||
|
scheduler.schedule(() => service.getTargets('testId', options).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -17,13 +17,14 @@ import { RestResponse } from '../cache/response.models';
|
|||||||
import { RequestEntry } from '../data/request-entry.model';
|
import { RequestEntry } from '../data/request-entry.model';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
import { GroupDataService } from '../eperson/group-data.service';
|
import { GroupDataService } from '../eperson/group-data.service';
|
||||||
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
|
|
||||||
describe('SupervisionOrderService', () => {
|
describe('SupervisionOrderService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: SupervisionOrderDataService;
|
let service: SupervisionOrderDataService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheServiceStub;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
let groupService: GroupDataService;
|
let groupService: GroupDataService;
|
||||||
@@ -127,14 +128,14 @@ describe('SupervisionOrderService', () => {
|
|||||||
a: 'https://rest.api/rest/api/group/groups/' + groupUUID
|
a: 'https://rest.api/rest/api/group/groups/' + groupUUID
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = new ObjectCacheServiceStub();
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
|
|
||||||
service = new SupervisionOrderDataService(
|
service = new SupervisionOrderDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache as ObjectCacheService,
|
||||||
halService,
|
halService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
comparator,
|
comparator,
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
<ds-dso-edit-metadata-value *ngFor="let mdValue of form.fields[mdField]; let idx = index" role="presentation"
|
<ds-dso-edit-metadata-value *ngFor="let mdValue of form.fields[mdField]; let idx = index" role="presentation"
|
||||||
[dso]="dso"
|
[dso]="dso"
|
||||||
[mdValue]="mdValue"
|
[mdValue]="mdValue"
|
||||||
|
[mdField]="mdField"
|
||||||
[dsoType]="dsoType"
|
[dsoType]="dsoType"
|
||||||
[saving$]="saving$"
|
[saving$]="saving$"
|
||||||
[isOnlyValue]="form.fields[mdField].length === 1"
|
[isOnlyValue]="form.fields[mdField].length === 1"
|
||||||
|
@@ -75,7 +75,8 @@ export class DsoEditMetadataValue {
|
|||||||
confirmChanges(finishEditing = false) {
|
confirmChanges(finishEditing = false) {
|
||||||
this.reordered = this.originalValue.place !== this.newValue.place;
|
this.reordered = this.originalValue.place !== this.newValue.place;
|
||||||
if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) {
|
if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) {
|
||||||
if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language)) {
|
if (this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language
|
||||||
|
|| this.originalValue.authority !== this.newValue.authority || this.originalValue.confidence !== this.newValue.confidence) {
|
||||||
this.change = DsoEditMetadataChangeType.UPDATE;
|
this.change = DsoEditMetadataChangeType.UPDATE;
|
||||||
} else {
|
} else {
|
||||||
this.change = undefined;
|
this.change = undefined;
|
||||||
@@ -404,10 +405,13 @@ export class DsoEditMetadataForm {
|
|||||||
if (hasValue(value.change)) {
|
if (hasValue(value.change)) {
|
||||||
if (value.change === DsoEditMetadataChangeType.UPDATE) {
|
if (value.change === DsoEditMetadataChangeType.UPDATE) {
|
||||||
// Only changes to value or language are considered "replace" operations. Changes to place are considered "move", which is processed below.
|
// Only changes to value or language are considered "replace" operations. Changes to place are considered "move", which is processed below.
|
||||||
if (value.originalValue.value !== value.newValue.value || value.originalValue.language !== value.newValue.language) {
|
if (value.originalValue.value !== value.newValue.value || value.originalValue.language !== value.newValue.language
|
||||||
|
|| value.originalValue.authority !== value.newValue.authority || value.originalValue.confidence !== value.newValue.confidence) {
|
||||||
replaceOperations.push(new MetadataPatchReplaceOperation(field, value.originalValue.place, {
|
replaceOperations.push(new MetadataPatchReplaceOperation(field, value.originalValue.place, {
|
||||||
value: value.newValue.value,
|
value: value.newValue.value,
|
||||||
language: value.newValue.language,
|
language: value.newValue.language,
|
||||||
|
authority: value.newValue.authority,
|
||||||
|
confidence: value.newValue.confidence
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} else if (value.change === DsoEditMetadataChangeType.REMOVE) {
|
} else if (value.change === DsoEditMetadataChangeType.REMOVE) {
|
||||||
@@ -416,6 +420,8 @@ export class DsoEditMetadataForm {
|
|||||||
addOperations.push(new MetadataPatchAddOperation(field, {
|
addOperations.push(new MetadataPatchAddOperation(field, {
|
||||||
value: value.newValue.value,
|
value: value.newValue.value,
|
||||||
language: value.newValue.language,
|
language: value.newValue.language,
|
||||||
|
authority: value.newValue.authority,
|
||||||
|
confidence: value.newValue.confidence
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
console.warn('Illegal metadata change state detected for', value);
|
console.warn('Illegal metadata change state detected for', value);
|
||||||
|
@@ -1,11 +1,58 @@
|
|||||||
<div class="d-flex flex-row ds-value-row" *ngVar="mdValue.newValue.isVirtual as isVirtual" role="row"
|
<div class="d-flex flex-row ds-value-row" *ngVar="mdValue.newValue.isVirtual as isVirtual" role="row"
|
||||||
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
|
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
|
||||||
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
||||||
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex align-items-center" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
||||||
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
|
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
|
||||||
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation" [(ngModel)]="mdValue.newValue.value"
|
<textarea class="form-control" rows="5" *ngIf="mdValue.editing && !mdRepresentation && !(isAuthorityControlled() | async)" [(ngModel)]="mdValue.newValue.value"
|
||||||
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.value') | translate"
|
||||||
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
|
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
|
||||||
|
<ds-dynamic-scrollable-dropdown *ngIf="mdValue.editing && (isScrollableVocabulary() | async)"
|
||||||
|
[bindId]="mdField"
|
||||||
|
[group]="group"
|
||||||
|
[model]="getModel() | async"
|
||||||
|
(change)="onChangeAuthorityField($event)">
|
||||||
|
</ds-dynamic-scrollable-dropdown>
|
||||||
|
<ds-dynamic-onebox *ngIf="mdValue.editing && ((isHierarchicalVocabulary() | async) || (isSuggesterVocabulary() | async))"
|
||||||
|
[group]="group"
|
||||||
|
[model]="getModel() | async"
|
||||||
|
(change)="onChangeAuthorityField($event)">
|
||||||
|
</ds-dynamic-onebox>
|
||||||
|
<div *ngIf="!isVirtual && !mdValue.editing && mdValue.newValue.authority && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_UNSET && mdValue.newValue.confidence !== ConfidenceTypeEnum.CF_NOVALUE">
|
||||||
|
<span class="badge badge-light border" >
|
||||||
|
<i dsAuthorityConfidenceState
|
||||||
|
class="fas fa-fw p-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
[authorityValue]="mdValue.newValue"
|
||||||
|
[iconMode]="true"
|
||||||
|
></i>
|
||||||
|
{{ dsoType + '.edit.metadata.authority.label' | translate }} {{ mdValue.newValue.authority }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2" *ngIf=" mdValue.editing && (isAuthorityControlled() | async) && (isSuggesterVocabulary() | async)">
|
||||||
|
<div class="btn-group w-75">
|
||||||
|
<i dsAuthorityConfidenceState
|
||||||
|
class="fas fa-fw p-0 mr-1 mt-auto mb-auto"
|
||||||
|
aria-hidden="true"
|
||||||
|
[authorityValue]="mdValue.newValue.confidence"
|
||||||
|
[iconMode]="true"
|
||||||
|
></i>
|
||||||
|
<input class="form-control form-outline" [(ngModel)]="mdValue.newValue.authority" [disabled]="!editingAuthority"
|
||||||
|
[attr.aria-label]="(dsoType + '.edit.metadata.edit.authority.key') | translate"
|
||||||
|
(change)="onChangeAuthorityKey()" />
|
||||||
|
<button class="btn btn-outline-secondary btn-sm ng-star-inserted" id="metadata-confirm-btn" *ngIf="!editingAuthority"
|
||||||
|
[title]="dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate"
|
||||||
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.open-authority-edition' | translate }}"
|
||||||
|
(click)="onChangeEditingAuthorityStatus(true)">
|
||||||
|
<i class="fas fa-lock fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-success btn-sm ng-star-inserted" id="metadata-confirm-btn" *ngIf="editingAuthority"
|
||||||
|
[title]="dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate"
|
||||||
|
ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.close-authority-edition' | translate }}"
|
||||||
|
(click)="onChangeEditingAuthorityStatus(false)">
|
||||||
|
<i class="fas fa-lock-open fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex" *ngIf="mdRepresentation">
|
<div class="d-flex" *ngIf="mdRepresentation">
|
||||||
<a class="mr-2" target="_blank" [routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
|
<a class="mr-2" target="_blank" [routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
|
||||||
<ds-themed-type-badge [object]="mdRepresentation"></ds-themed-type-badge>
|
<ds-themed-type-badge [object]="mdRepresentation"></ds-themed-type-badge>
|
||||||
@@ -45,8 +92,6 @@
|
|||||||
[disabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
|
[disabled]="isVirtual || (!mdValue.change && mdValue.reordered) || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
|
||||||
<i class="fas fa-undo-alt fa-fw"></i>
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
|
<button class="btn btn-outline-secondary ds-drag-handle btn-sm" data-test="metadata-drag-btn" *ngVar="(isOnlyValue || (saving$ | async)) as disabled"
|
||||||
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [disabled]="disabled"
|
cdkDragHandle [cdkDragHandleDisabled]="disabled" [ngClass]="{'disabled': disabled}" [disabled]="disabled"
|
||||||
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
[title]="dsoType + '.edit.metadata.edit.buttons.drag' | translate"
|
||||||
@@ -55,4 +100,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,6 +11,23 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
|
|||||||
import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
|
import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
|
||||||
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { Vocabulary } from 'src/app/core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyServiceStub } from 'src/app/shared/testing/vocabulary-service.stub';
|
||||||
|
import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { ConfidenceType } from 'src/app/core/shared/confidence-type';
|
||||||
|
import { DynamicOneboxModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { RegistryService } from 'src/app/core/registry/registry.service';
|
||||||
|
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
||||||
|
import { createPaginatedList } from 'src/app/shared/testing/utils.test';
|
||||||
|
import { MetadataField } from 'src/app/core/metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model';
|
||||||
|
|
||||||
const EDIT_BTN = 'edit';
|
const EDIT_BTN = 'edit';
|
||||||
const CONFIRM_BTN = 'confirm';
|
const CONFIRM_BTN = 'confirm';
|
||||||
@@ -24,17 +41,111 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
|
|
||||||
let relationshipService: RelationshipDataService;
|
let relationshipService: RelationshipDataService;
|
||||||
let dsoNameService: DSONameService;
|
let dsoNameService: DSONameService;
|
||||||
|
let vocabularyServiceStub: any;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let registryService: RegistryService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
let editMetadataValue: DsoEditMetadataValue;
|
let editMetadataValue: DsoEditMetadataValue;
|
||||||
let metadataValue: MetadataValue;
|
let metadataValue: MetadataValue;
|
||||||
|
let dso: DSpaceObject;
|
||||||
|
|
||||||
|
const collection = Object.assign(new Collection(), {
|
||||||
|
uuid: 'fake-uuid'
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'fake-item-url/item' }
|
||||||
|
},
|
||||||
|
id: 'item',
|
||||||
|
uuid: 'item',
|
||||||
|
owningCollection: createSuccessfulRemoteDataObject$(collection)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockVocabularyScrollable: Vocabulary = {
|
||||||
|
id: 'scrollable',
|
||||||
|
name: 'scrollable',
|
||||||
|
scrollable: true,
|
||||||
|
hierarchical: false,
|
||||||
|
preloadLevel: 0,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self'
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockVocabularyHierarchical: Vocabulary = {
|
||||||
|
id: 'hierarchical',
|
||||||
|
name: 'hierarchical',
|
||||||
|
scrollable: false,
|
||||||
|
hierarchical: true,
|
||||||
|
preloadLevel: 2,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self'
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockVocabularySuggester: Vocabulary = {
|
||||||
|
id: 'suggester',
|
||||||
|
name: 'suggester',
|
||||||
|
scrollable: false,
|
||||||
|
hierarchical: false,
|
||||||
|
preloadLevel: 0,
|
||||||
|
type: 'vocabulary',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'self'
|
||||||
|
},
|
||||||
|
entries: {
|
||||||
|
href: 'entries'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadataSchema: MetadataSchema;
|
||||||
|
let metadataFields: MetadataField[];
|
||||||
|
|
||||||
function initServices(): void {
|
function initServices(): void {
|
||||||
|
metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
|
id: 0,
|
||||||
|
prefix: 'metadata',
|
||||||
|
namespace: 'http://example.com/',
|
||||||
|
});
|
||||||
|
metadataFields = [
|
||||||
|
Object.assign(new MetadataField(), {
|
||||||
|
id: 0,
|
||||||
|
element: 'regular',
|
||||||
|
qualifier: null,
|
||||||
|
schema: createSuccessfulRemoteDataObject$(metadataSchema),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService', {
|
relationshipService = jasmine.createSpyObj('relationshipService', {
|
||||||
resolveMetadataRepresentation: of(new ItemMetadataRepresentation(metadataValue)),
|
resolveMetadataRepresentation: of(new ItemMetadataRepresentation(metadataValue)),
|
||||||
});
|
});
|
||||||
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||||
getName: 'Related Name',
|
getName: 'Related Name',
|
||||||
});
|
});
|
||||||
|
itemService = jasmine.createSpyObj('itemService', {
|
||||||
|
findByHref: createSuccessfulRemoteDataObject$(item)
|
||||||
|
});
|
||||||
|
vocabularyServiceStub = new VocabularyServiceStub();
|
||||||
|
registryService = jasmine.createSpyObj('registryService', {
|
||||||
|
queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)),
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -45,6 +156,11 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
authority: undefined,
|
authority: undefined,
|
||||||
});
|
});
|
||||||
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
dso = Object.assign(new DSpaceObject(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'fake-dso-url/dso' }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
initServices();
|
initServices();
|
||||||
|
|
||||||
@@ -54,6 +170,10 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: RelationshipDataService, useValue: relationshipService },
|
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||||
{ provide: DSONameService, useValue: dsoNameService },
|
{ provide: DSONameService, useValue: dsoNameService },
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyServiceStub },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: RegistryService, useValue: registryService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -63,6 +183,7 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
fixture = TestBed.createComponent(DsoEditMetadataValueComponent);
|
fixture = TestBed.createComponent(DsoEditMetadataValueComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.mdValue = editMetadataValue;
|
component.mdValue = editMetadataValue;
|
||||||
|
component.dso = dso;
|
||||||
component.saving$ = of(false);
|
component.saving$ = of(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -144,6 +265,222 @@ describe('DsoEditMetadataValueComponent', () => {
|
|||||||
assertButton(DRAG_BTN, true, false);
|
assertButton(DRAG_BTN, true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field not uses a vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(null, 204));
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Regular value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: null,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.regular';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render a textarea', () => {
|
||||||
|
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('textarea'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a scrollable vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyScrollable));
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: null,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.scrollable';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicScrollableDropdownComponent', () => {
|
||||||
|
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-scrollable-dropdown'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicScrollableDropdownModel', () => {
|
||||||
|
const result = component.getModel();
|
||||||
|
|
||||||
|
expect(result instanceof Observable).toBe(true);
|
||||||
|
|
||||||
|
result.subscribe((model) => {
|
||||||
|
expect(model instanceof DynamicScrollableDropdownModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a hierarchical vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularyHierarchical));
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: null,
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.hierarchical';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicOneboxComponent', () => {
|
||||||
|
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicOneboxModel', () => {
|
||||||
|
const result = component.getModel();
|
||||||
|
|
||||||
|
expect(result instanceof Observable).toBe(true);
|
||||||
|
|
||||||
|
result.subscribe((model) => {
|
||||||
|
expect(model instanceof DynamicOneboxModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the metadata field uses a suggester vocabulary and is editing', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(vocabularyServiceStub, 'getVocabularyByMetadataAndCollection').and.returnValue(createSuccessfulRemoteDataObject$(mockVocabularySuggester));
|
||||||
|
spyOn(component.confirm, 'emit');
|
||||||
|
metadataValue = Object.assign(new MetadataValue(), {
|
||||||
|
value: 'Authority Controlled value',
|
||||||
|
language: 'en',
|
||||||
|
place: 0,
|
||||||
|
authority: 'authority-key',
|
||||||
|
confidence: ConfidenceType.CF_UNCERTAIN
|
||||||
|
});
|
||||||
|
editMetadataValue = new DsoEditMetadataValue(metadataValue);
|
||||||
|
editMetadataValue.editing = true;
|
||||||
|
component.mdValue = editMetadataValue;
|
||||||
|
component.mdField = 'metadata.suggester';
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should render the DsDynamicOneboxComponent', () => {
|
||||||
|
expect(vocabularyServiceStub.getVocabularyByMetadataAndCollection).toHaveBeenCalled();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-dynamic-onebox'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getModel should return a DynamicOneboxModel', () => {
|
||||||
|
const result = component.getModel();
|
||||||
|
|
||||||
|
expect(result instanceof Observable).toBe(true);
|
||||||
|
|
||||||
|
result.subscribe((model) => {
|
||||||
|
expect(model instanceof DynamicOneboxModel).toBe(true);
|
||||||
|
expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authority key edition', () => {
|
||||||
|
|
||||||
|
it('should update confidence to CF_NOVALUE when authority is cleared', () => {
|
||||||
|
component.mdValue.newValue.authority = '';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_NOVALUE);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update confidence to CF_ACCEPTED when authority key is edited', () => {
|
||||||
|
component.mdValue.newValue.authority = 'newAuthority';
|
||||||
|
component.mdValue.originalValue.authority = 'oldAuthority';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update confidence when authority key remains the same', () => {
|
||||||
|
component.mdValue.newValue.authority = 'sameAuthority';
|
||||||
|
component.mdValue.originalValue.authority = 'sameAuthority';
|
||||||
|
|
||||||
|
component.onChangeAuthorityKey();
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNCERTAIN);
|
||||||
|
expect(component.confirm.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChangeEditingAuthorityStatus with true when clicking the lock button', () => {
|
||||||
|
spyOn(component, 'onChangeEditingAuthorityStatus');
|
||||||
|
const lockButton = fixture.nativeElement.querySelector('#metadata-confirm-btn');
|
||||||
|
|
||||||
|
lockButton.click();
|
||||||
|
|
||||||
|
expect(component.onChangeEditingAuthorityStatus).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable the input when editingAuthority is false', () => {
|
||||||
|
component.editingAuthority = false;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const inputElement = fixture.nativeElement.querySelector('input');
|
||||||
|
expect(inputElement.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable the input when editingAuthority is true', () => {
|
||||||
|
component.editingAuthority = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const inputElement = fixture.nativeElement.querySelector('input');
|
||||||
|
expect(inputElement.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update mdValue.newValue properties when authority is present', () => {
|
||||||
|
const event = {
|
||||||
|
value: 'Some value',
|
||||||
|
authority: 'Some authority',
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onChangeAuthorityField(event);
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.value).toBe(event.value);
|
||||||
|
expect(component.mdValue.newValue.authority).toBe(event.authority);
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_ACCEPTED);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update mdValue.newValue properties when authority is not present', () => {
|
||||||
|
const event = {
|
||||||
|
value: 'Some value',
|
||||||
|
authority: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onChangeAuthorityField(event);
|
||||||
|
|
||||||
|
expect(component.mdValue.newValue.value).toBe(event.value);
|
||||||
|
expect(component.mdValue.newValue.authority).toBeNull();
|
||||||
|
expect(component.mdValue.newValue.confidence).toBe(ConfidenceType.CF_UNSET);
|
||||||
|
expect(component.confirm.emit).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
function assertButton(name: string, exists: boolean, disabled: boolean = false): void {
|
function assertButton(name: string, exists: boolean, disabled: boolean = false): void {
|
||||||
describe(`${name} button`, () => {
|
describe(`${name} button`, () => {
|
||||||
let btn: DebugElement;
|
let btn: DebugElement;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||||
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import {
|
import {
|
||||||
@@ -8,10 +8,28 @@ import {
|
|||||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { EMPTY } from 'rxjs/internal/observable/empty';
|
import { EMPTY } from 'rxjs/internal/observable/empty';
|
||||||
|
import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { Vocabulary } from '../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||||
|
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, metadataFieldsToString } from '../../../core/shared/operators';
|
||||||
|
import { DsDynamicOneboxModelConfig, DynamicOneboxModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
|
||||||
|
import { DynamicScrollableDropdownModel, DynamicScrollableDropdownModelConfig } from '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RegistryService } from 'src/app/core/registry/registry.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dso-edit-metadata-value',
|
selector: 'ds-dso-edit-metadata-value',
|
||||||
@@ -21,7 +39,7 @@ import { EMPTY } from 'rxjs/internal/observable/empty';
|
|||||||
/**
|
/**
|
||||||
* Component displaying a single editable row for a metadata value
|
* Component displaying a single editable row for a metadata value
|
||||||
*/
|
*/
|
||||||
export class DsoEditMetadataValueComponent implements OnInit {
|
export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
||||||
/**
|
/**
|
||||||
* The parent {@link DSpaceObject} to display a metadata form for
|
* The parent {@link DSpaceObject} to display a metadata form for
|
||||||
* Also used to determine metadata-representations in case of virtual metadata
|
* Also used to determine metadata-representations in case of virtual metadata
|
||||||
@@ -51,6 +69,11 @@ export class DsoEditMetadataValueComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() isOnlyValue = false;
|
@Input() isOnlyValue = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetadataField to edit
|
||||||
|
*/
|
||||||
|
@Input() mdField?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits when the user clicked edit
|
* Emits when the user clicked edit
|
||||||
*/
|
*/
|
||||||
@@ -82,6 +105,12 @@ export class DsoEditMetadataValueComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ConfidenceType enumeration for access in the component's template
|
||||||
|
* @type {ConfidenceType}
|
||||||
|
*/
|
||||||
|
public ConfidenceTypeEnum = ConfidenceType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item this metadata value represents in case it's virtual (if any, otherwise null)
|
* The item this metadata value represents in case it's virtual (if any, otherwise null)
|
||||||
*/
|
*/
|
||||||
@@ -97,12 +126,48 @@ export class DsoEditMetadataValueComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
mdRepresentationName$: Observable<string | null>;
|
mdRepresentationName$: Observable<string | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the authority field is currently being edited
|
||||||
|
*/
|
||||||
|
public editingAuthority = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field group used by authority field
|
||||||
|
* @type {UntypedFormGroup}
|
||||||
|
*/
|
||||||
|
group = new UntypedFormGroup({ authorityField : new UntypedFormControl()});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable property of the model to use for editinf authorities values
|
||||||
|
*/
|
||||||
|
private model$: Observable<DynamicOneboxModel | DynamicScrollableDropdownModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable with information about the authority vocabulary used
|
||||||
|
*/
|
||||||
|
private vocabulary$: Observable<Vocabulary>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observables with information about the authority vocabulary type used
|
||||||
|
*/
|
||||||
|
private isAuthorityControlled$: Observable<boolean>;
|
||||||
|
private isHierarchicalVocabulary$: Observable<boolean>;
|
||||||
|
private isScrollableVocabulary$: Observable<boolean>;
|
||||||
|
private isSuggesterVocabulary$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(protected relationshipService: RelationshipDataService,
|
constructor(protected relationshipService: RelationshipDataService,
|
||||||
protected dsoNameService: DSONameService) {
|
protected dsoNameService: DSONameService,
|
||||||
|
protected vocabularyService: VocabularyService,
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected cdr: ChangeDetectorRef,
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translate: TranslateService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initVirtualProperties();
|
this.initVirtualProperties();
|
||||||
|
this.initAuthorityProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,4 +188,223 @@ export class DsoEditMetadataValueComponent implements OnInit {
|
|||||||
map((mdRepresentation: ItemMetadataRepresentation) => mdRepresentation ? this.dsoNameService.getName(mdRepresentation) : null),
|
map((mdRepresentation: ItemMetadataRepresentation) => mdRepresentation ? this.dsoNameService.getName(mdRepresentation) : null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise potential properties of a authority controlled metadata field
|
||||||
|
*/
|
||||||
|
initAuthorityProperties(): void {
|
||||||
|
|
||||||
|
if (isNotEmpty(this.mdField)) {
|
||||||
|
|
||||||
|
const owningCollection$: Observable<Collection> = this.itemService.findByHref(this.dso._links.self.href, true, true, followLink('owningCollection'))
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
switchMap((item: Item) => item.owningCollection),
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.vocabulary$ = owningCollection$.pipe(
|
||||||
|
switchMap((c: Collection) => this.vocabularyService
|
||||||
|
.getVocabularyByMetadataAndCollection(this.mdField, c.uuid)
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.vocabulary$ = observableOf(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isAuthorityControlled$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isHierarchicalVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && result.hierarchical)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isScrollableVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && result.scrollable)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isSuggesterVocabulary$ = this.vocabulary$.pipe(
|
||||||
|
map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.model$ = this.vocabulary$.pipe(
|
||||||
|
map((vocabulary: Vocabulary) => {
|
||||||
|
let formFieldValue;
|
||||||
|
if (isNotEmpty(this.mdValue.newValue.value)) {
|
||||||
|
formFieldValue = new FormFieldMetadataValueObject();
|
||||||
|
formFieldValue.value = this.mdValue.newValue.value;
|
||||||
|
formFieldValue.display = this.mdValue.newValue.value;
|
||||||
|
if (this.mdValue.newValue.authority) {
|
||||||
|
formFieldValue.authority = this.mdValue.newValue.authority;
|
||||||
|
formFieldValue.confidence = this.mdValue.newValue.confidence;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formFieldValue = this.mdValue.newValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vocabularyOptions = vocabulary ? {
|
||||||
|
closed: false,
|
||||||
|
name: vocabulary.name
|
||||||
|
} as VocabularyOptions : null;
|
||||||
|
|
||||||
|
if (!vocabulary.scrollable) {
|
||||||
|
let model: DsDynamicOneboxModelConfig = {
|
||||||
|
id: 'authorityField',
|
||||||
|
label: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
vocabularyOptions: vocabularyOptions,
|
||||||
|
metadataFields: [this.mdField],
|
||||||
|
value: formFieldValue,
|
||||||
|
repeatable: false,
|
||||||
|
submissionId: 'edit-metadata',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
};
|
||||||
|
return new DynamicOneboxModel(model);
|
||||||
|
} else {
|
||||||
|
let model: DynamicScrollableDropdownModelConfig = {
|
||||||
|
id: 'authorityField',
|
||||||
|
label: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
placeholder: `${this.dsoType}.edit.metadata.edit.value`,
|
||||||
|
vocabularyOptions: vocabularyOptions,
|
||||||
|
metadataFields: [this.mdField],
|
||||||
|
value: formFieldValue,
|
||||||
|
repeatable: false,
|
||||||
|
submissionId: 'edit-metadata',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
maxOptions: 10
|
||||||
|
};
|
||||||
|
return new DynamicScrollableDropdownModel(model);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change callback for the component. Check if the mdField has changed to retrieve whether it is metadata
|
||||||
|
* that uses a controlled vocabulary and update the related properties
|
||||||
|
*
|
||||||
|
* @param {SimpleChanges} changes
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (isNotEmpty(changes.mdField) && !changes.mdField.firstChange) {
|
||||||
|
if (isNotEmpty(changes.mdField.currentValue) ) {
|
||||||
|
if (isNotEmpty(changes.mdField.previousValue) &&
|
||||||
|
changes.mdField.previousValue !== changes.mdField.currentValue) {
|
||||||
|
// Clear authority value in case it has been assigned with the previous metadataField used
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only ask if the current mdField have a period character to reduce request
|
||||||
|
if (changes.mdField.currentValue.includes('.')) {
|
||||||
|
this.validateMetadataField().subscribe((isValid: boolean) => {
|
||||||
|
if (isValid) {
|
||||||
|
this.initAuthorityProperties();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the metadata field to check if it exists on the server and return an observable boolean for success/error
|
||||||
|
*/
|
||||||
|
validateMetadataField(): Observable<boolean> {
|
||||||
|
return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return observableOf(rd).pipe(
|
||||||
|
metadataFieldsToString(),
|
||||||
|
take(1),
|
||||||
|
map((fields: string[]) => fields.indexOf(this.mdField) > -1)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage);
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this field use a authority vocabulary
|
||||||
|
*/
|
||||||
|
isAuthorityControlled(): Observable<boolean> {
|
||||||
|
return this.isAuthorityControlled$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if configured vocabulary is Hierarchical or not
|
||||||
|
*/
|
||||||
|
isHierarchicalVocabulary(): Observable<boolean> {
|
||||||
|
return this.isHierarchicalVocabulary$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if configured vocabulary is Scrollable or not
|
||||||
|
*/
|
||||||
|
isScrollableVocabulary(): Observable<boolean> {
|
||||||
|
return this.isScrollableVocabulary$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if configured vocabulary is Suggester or not
|
||||||
|
* (a vocabulary not Scrollable and not Hierarchical that uses an autocomplete field)
|
||||||
|
*/
|
||||||
|
isSuggesterVocabulary(): Observable<boolean> {
|
||||||
|
return this.isSuggesterVocabulary$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the change of authority field value updating the authority key and confidence as necessary
|
||||||
|
*/
|
||||||
|
onChangeAuthorityField(event): void {
|
||||||
|
this.mdValue.newValue.value = event.value;
|
||||||
|
if (event.authority) {
|
||||||
|
this.mdValue.newValue.authority = event.authority;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
||||||
|
} else {
|
||||||
|
this.mdValue.newValue.authority = null;
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_UNSET;
|
||||||
|
}
|
||||||
|
this.confirm.emit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used
|
||||||
|
* for the authority field
|
||||||
|
*/
|
||||||
|
getModel(): Observable<DynamicOneboxModel | DynamicScrollableDropdownModel> {
|
||||||
|
return this.model$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the status of the editingAuthority property
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
onChangeEditingAuthorityStatus(status: boolean) {
|
||||||
|
this.editingAuthority = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the change in authority value, updating the confidence as necessary.
|
||||||
|
* If the authority key is cleared, the confidence is set to {@link ConfidenceType.CF_NOVALUE}.
|
||||||
|
* If the authority key is edited and differs from the original, the confidence is set to {@link ConfidenceType.CF_ACCEPTED}.
|
||||||
|
*/
|
||||||
|
onChangeAuthorityKey() {
|
||||||
|
if (this.mdValue.newValue.authority === '') {
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_NOVALUE;
|
||||||
|
this.confirm.emit(false);
|
||||||
|
} else if (this.mdValue.newValue.authority !== this.mdValue.originalValue.authority) {
|
||||||
|
this.mdValue.newValue.confidence = ConfidenceType.CF_ACCEPTED;
|
||||||
|
this.confirm.emit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -40,6 +40,7 @@
|
|||||||
[dsoType]="dsoType"
|
[dsoType]="dsoType"
|
||||||
[saving$]="savingOrLoadingFieldValidation$"
|
[saving$]="savingOrLoadingFieldValidation$"
|
||||||
[isOnlyValue]="true"
|
[isOnlyValue]="true"
|
||||||
|
[mdField]="newMdField"
|
||||||
(confirm)="confirmNewValue($event)"
|
(confirm)="confirmNewValue($event)"
|
||||||
(remove)="form.newValue = undefined"
|
(remove)="form.newValue = undefined"
|
||||||
(undo)="form.newValue = undefined">
|
(undo)="form.newValue = undefined">
|
||||||
|
@@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
|||||||
import {
|
import {
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { UpdateDataService } from '../../core/data/update-data.service';
|
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@@ -22,6 +21,7 @@ import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analy
|
|||||||
import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator';
|
import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALDataService } from '../../core/data/base/hal-data-service.interface';
|
import { HALDataService } from '../../core/data/base/hal-data-service.interface';
|
||||||
|
import { UpdateDataService } from '../../core/data/update-data.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dso-edit-metadata',
|
selector: 'ds-dso-edit-metadata',
|
||||||
|
@@ -7,10 +7,12 @@ import { DsoEditMetadataValueComponent } from './dso-edit-metadata/dso-edit-meta
|
|||||||
import { DsoEditMetadataHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component';
|
import { DsoEditMetadataHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component';
|
||||||
import { DsoEditMetadataValueHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-value-headers/dso-edit-metadata-value-headers.component';
|
import { DsoEditMetadataValueHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-value-headers/dso-edit-metadata-value-headers.component';
|
||||||
import { ThemedDsoEditMetadataComponent } from './dso-edit-metadata/themed-dso-edit-metadata.component';
|
import { ThemedDsoEditMetadataComponent } from './dso-edit-metadata/themed-dso-edit-metadata.component';
|
||||||
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
FormModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
DsoEditMetadataComponent,
|
DsoEditMetadataComponent,
|
||||||
|
@@ -32,6 +32,18 @@
|
|||||||
</ds-generic-item-page-field>
|
</ds-generic-item-page-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-7">
|
<div class="col-xs-12 col-md-7">
|
||||||
|
<ds-item-page-img-field
|
||||||
|
[fields]="['organization.identifier.ror']"
|
||||||
|
[img]="{
|
||||||
|
URI: './assets/images/ror-icon.svg',
|
||||||
|
alt: 'item.page.image.alt.ROR',
|
||||||
|
heightVar: '--ds-item-page-img-field-ror-inline-height'
|
||||||
|
}"
|
||||||
|
[item]="object"
|
||||||
|
[label]="'orgunit.page.ror'"
|
||||||
|
[urlRegex]="'(.*)ror.org'"
|
||||||
|
>
|
||||||
|
</ds-item-page-img-field>
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isPublicationOfOrgUnit'"
|
[relationType]="'isPublicationOfOrgUnit'"
|
||||||
|
@@ -1,9 +1,19 @@
|
|||||||
<ds-themed-home-news></ds-themed-home-news>
|
<ds-themed-home-news></ds-themed-home-news>
|
||||||
<div class="container">
|
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'container-fluid' : 'container'">
|
||||||
|
<div class="row m-5">
|
||||||
|
<div class="col-sm-3" *ngIf="appConfig.homePage.showDiscoverFilters">
|
||||||
|
<ds-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
|
||||||
|
[inPlaceSearch]="false" [showScopeSelector]="false"></ds-configuration-search-page>
|
||||||
|
</div>
|
||||||
|
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'col-sm-9' : 'col-sm-12'">
|
||||||
<ng-container *ngIf="(site$ | async) as site">
|
<ng-container *ngIf="(site$ | async) as site">
|
||||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-themed-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
|
<ds-themed-search-form [inPlaceSearch]="false"
|
||||||
|
[searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-themed-search-form>
|
||||||
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
||||||
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ds-suggestions-popup></ds-suggestions-popup>
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Site } from '../core/shared/site.model';
|
import { Site } from '../core/shared/site.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-home-page',
|
selector: 'ds-home-page',
|
||||||
styleUrls: ['./home-page.component.scss'],
|
styleUrls: ['./home-page.component.scss'],
|
||||||
@@ -14,6 +15,7 @@ export class HomePageComponent implements OnInit {
|
|||||||
site$: Observable<Site>;
|
site$: Observable<Site>;
|
||||||
recentSubmissionspageSize: number;
|
recentSubmissionspageSize: number;
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;
|
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;
|
||||||
|
@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { HomeNewsComponent } from './home-news/home-news.component';
|
import { HomeNewsComponent } from './home-news/home-news.component';
|
||||||
import { HomePageRoutingModule } from './home-page-routing.module';
|
import { HomePageRoutingModule } from './home-page-routing.module';
|
||||||
|
|
||||||
import { HomePageComponent } from './home-page.component';
|
import { HomePageComponent } from './home-page.component';
|
||||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
@@ -13,6 +12,8 @@ import { RecentItemListComponent } from './recent-item-list/recent-item-list.com
|
|||||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-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 { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
|
||||||
|
import { SearchModule } from '../shared/search/search.module';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
HomePageComponent,
|
HomePageComponent,
|
||||||
@@ -28,10 +29,12 @@ const DECLARATIONS = [
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule.withEntryComponents(),
|
SharedModule.withEntryComponents(),
|
||||||
|
SearchModule,
|
||||||
JournalEntitiesModule.withEntryComponents(),
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
ResearchEntitiesModule.withEntryComponents(),
|
ResearchEntitiesModule.withEntryComponents(),
|
||||||
HomePageRoutingModule,
|
HomePageRoutingModule,
|
||||||
StatisticsModule.forRoot()
|
StatisticsModule.forRoot(),
|
||||||
|
NotificationsModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
<!--
|
<!--
|
||||||
Choose a template. Priority: markdown, link, browse link.
|
Choose a template. Priority: markdown, link, browse link.
|
||||||
-->
|
-->
|
||||||
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? link : (hasBrowseDefinition() ? browselink : simple)));
|
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? (img != null ? linkImg : link) : (hasBrowseDefinition() ? browselink : simple)));
|
||||||
context: {value: mdValue.value}">
|
context: {value: mdValue.value, img}">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
|
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -23,6 +23,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Render value as a link with icon -->
|
||||||
|
<ng-template #linkImg let-img="img" let-value="value">
|
||||||
|
<a [href]="value" class="link-anchor dont-break-out ds-simple-metadata-link" target="_blank">
|
||||||
|
<img class="link-logo"
|
||||||
|
[alt]="img.alt | translate"
|
||||||
|
[style.height]="'var(' + img.heightVar + ', --ds-item-page-img-field-default-inline-height)'"
|
||||||
|
[src]="img.URI"/>
|
||||||
|
{{value}}
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<!-- Render simple value in a span -->
|
<!-- Render simple value in a span -->
|
||||||
<ng-template #simple let-value="value">
|
<ng-template #simple let-value="value">
|
||||||
<span class="dont-break-out preserve-line-breaks">{{value}}</span>
|
<span class="dont-break-out preserve-line-breaks">{{value}}</span>
|
||||||
|
@@ -4,6 +4,7 @@ import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
|||||||
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type';
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type';
|
||||||
|
import { ImageField } from '../../simple/field-components/specific-field/item-page-field.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||||
@@ -55,6 +56,11 @@ export class MetadataValuesComponent implements OnChanges {
|
|||||||
|
|
||||||
@Input() browseDefinition?: BrowseDefinition;
|
@Input() browseDefinition?: BrowseDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional {@code ImageField} reference that represents an image to be displayed inline.
|
||||||
|
*/
|
||||||
|
@Input() img?: ImageField;
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
|
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,36 @@
|
|||||||
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
import {
|
||||||
|
RelatedEntitiesSearchComponent
|
||||||
|
} from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||||
import { SearchModule } from '../shared/search/search.module';
|
import { SearchModule } from '../shared/search/search.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { DYNAMIC_FORM_CONTROL_MAP_FN } from '@ng-dynamic-forms/core';
|
import { DYNAMIC_FORM_CONTROL_MAP_FN } from '@ng-dynamic-forms/core';
|
||||||
import { dsDynamicFormControlMapFn } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
import {
|
||||||
import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
|
dsDynamicFormControlMapFn
|
||||||
import { ItemVersionsDeleteModalComponent } from './versions/item-versions-delete-modal/item-versions-delete-modal.component';
|
} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
|
||||||
import { ItemVersionsSummaryModalComponent } from './versions/item-versions-summary-modal/item-versions-summary-modal.component';
|
import {
|
||||||
|
TabbedRelatedEntitiesSearchComponent
|
||||||
|
} from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
|
||||||
|
import {
|
||||||
|
ItemVersionsDeleteModalComponent
|
||||||
|
} from './versions/item-versions-delete-modal/item-versions-delete-modal.component';
|
||||||
|
import {
|
||||||
|
ItemVersionsSummaryModalComponent
|
||||||
|
} from './versions/item-versions-summary-modal/item-versions-summary-modal.component';
|
||||||
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
||||||
import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component';
|
import {
|
||||||
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
GenericItemPageFieldComponent
|
||||||
|
} from './simple/field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
|
import {
|
||||||
|
MetadataRepresentationListComponent
|
||||||
|
} from './simple/metadata-representation-list/metadata-representation-list.component';
|
||||||
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
|
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
|
||||||
import {
|
import {
|
||||||
ThemedMetadataRepresentationListComponent
|
ThemedMetadataRepresentationListComponent
|
||||||
} from './simple/metadata-representation-list/themed-metadata-representation-list.component';
|
} from './simple/metadata-representation-list/themed-metadata-representation-list.component';
|
||||||
|
import { ItemPageImgFieldComponent } from './simple/field-components/specific-field/img/item-page-img-field.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
ItemVersionsDeleteModalComponent,
|
ItemVersionsDeleteModalComponent,
|
||||||
@@ -32,6 +47,7 @@ const COMPONENTS = [
|
|||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
ThemedMetadataRepresentationListComponent,
|
ThemedMetadataRepresentationListComponent,
|
||||||
RelatedItemsComponent,
|
RelatedItemsComponent,
|
||||||
|
ItemPageImgFieldComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -42,7 +58,8 @@ const COMPONENTS = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
TranslateModule
|
TranslateModule,
|
||||||
|
NgOptimizedImage
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...COMPONENTS
|
...COMPONENTS
|
||||||
|
@@ -2,7 +2,10 @@
|
|||||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<ds-themed-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
|
<ds-themed-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
|
||||||
<span>{{ dsoNameService.getName(file) }}</span>
|
<span>
|
||||||
|
<span *ngIf="primaryBitsreamId === file.id" class="badge badge-primary">{{ 'item.page.bitstreams.primary' | translate }}</span>
|
||||||
|
{{ dsoNameService.getName(file) }}
|
||||||
|
</span>
|
||||||
<span> ({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span> ({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
</ds-themed-file-download-link>
|
</ds-themed-file-download-link>
|
||||||
|
@@ -25,7 +25,8 @@ describe('FileSectionComponent', () => {
|
|||||||
let fixture: ComponentFixture<FileSectionComponent>;
|
let fixture: ComponentFixture<FileSectionComponent>;
|
||||||
|
|
||||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([]))
|
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
findPrimaryBitstreamByItemAndName: observableOf(null)
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(),
|
const mockBitstream: Bitstream = Object.assign(new Bitstream(),
|
||||||
@@ -81,6 +82,20 @@ describe('FileSectionComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should set the id of primary bitstream', () => {
|
||||||
|
comp.primaryBitsreamId = undefined;
|
||||||
|
bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(mockBitstream));
|
||||||
|
comp.ngOnInit();
|
||||||
|
expect(comp.primaryBitsreamId).toBe(mockBitstream.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set the id of primary bitstream', () => {
|
||||||
|
comp.primaryBitsreamId = undefined;
|
||||||
|
bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(null));
|
||||||
|
comp.ngOnInit();
|
||||||
|
expect(comp.primaryBitsreamId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
describe('when the bitstreams are loading', () => {
|
describe('when the bitstreams are loading', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.bitstreams$.next([mockBitstream]);
|
comp.bitstreams$.next([mockBitstream]);
|
||||||
|
@@ -39,6 +39,8 @@ export class FileSectionComponent implements OnInit {
|
|||||||
|
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
|
||||||
|
primaryBitsreamId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
@@ -50,9 +52,19 @@ export class FileSectionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.getPrimaryBitstreamId();
|
||||||
this.getNextPage();
|
this.getNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPrimaryBitstreamId() {
|
||||||
|
this.bitstreamDataService.findPrimaryBitstreamByItemAndName(this.item, 'ORIGINAL', true, true).subscribe((primaryBitstream: Bitstream | null) => {
|
||||||
|
if (!primaryBitstream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.primaryBitsreamId = primaryBitstream?.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
||||||
* It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the
|
* It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the
|
||||||
|
@@ -0,0 +1,85 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ItemPageImgFieldComponent } from './item-page-img-field.component';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
|
||||||
|
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
|
||||||
|
import { GenericItemPageFieldComponent } from '../generic/generic-item-page-field.component';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ImageField } from '../item-page-field.component';
|
||||||
|
|
||||||
|
let component: ItemPageImgFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageImgFieldComponent>;
|
||||||
|
|
||||||
|
const mockField = 'organization.identifier.ror';
|
||||||
|
const mockValue = 'http://ror.org/awesome-identifier';
|
||||||
|
const mockLabel = 'ROR label';
|
||||||
|
const mockUrlRegex = '(.*)ror.org';
|
||||||
|
const mockImg = {
|
||||||
|
URI: './assets/images/ror-icon.svg',
|
||||||
|
alt: 'item.page.image.alt.ROR',
|
||||||
|
heightVar: '--ds-item-page-img-field-ror-inline-height'
|
||||||
|
} as ImageField;
|
||||||
|
|
||||||
|
describe('ItemPageImgFieldComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
providers: [
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
|
||||||
|
],
|
||||||
|
declarations: [ItemPageImgFieldComponent, GenericItemPageFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.overrideComponent(GenericItemPageFieldComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemPageImgFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
|
||||||
|
component.fields = [mockField];
|
||||||
|
component.label = mockLabel;
|
||||||
|
component.urlRegex = mockUrlRegex;
|
||||||
|
component.img = mockImg;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display display img tag', () => {
|
||||||
|
const image = fixture.debugElement.query(By.css('img.link-logo'));
|
||||||
|
expect(image).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have right attributes', () => {
|
||||||
|
const image = fixture.debugElement.query(By.css('img.link-logo'));
|
||||||
|
expect(image.attributes.src).toEqual(mockImg.URI);
|
||||||
|
expect(image.attributes.alt).toEqual(mockImg.alt);
|
||||||
|
|
||||||
|
const imageEl = image.nativeElement;
|
||||||
|
expect(imageEl.style.height).toContain(mockImg.heightVar);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the right value', () => {
|
||||||
|
const imageAnchor = fixture.debugElement.query(By.css('a.link-anchor'));
|
||||||
|
const anchorEl = imageAnchor.nativeElement;
|
||||||
|
expect(anchorEl.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ImageField, ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-page-img-field',
|
||||||
|
templateUrl: '../item-page-field.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that renders an inline image for a given field.
|
||||||
|
* This component uses a given {@code ImageField} configuration to correctly render the img.
|
||||||
|
*/
|
||||||
|
export class ItemPageImgFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
*/
|
||||||
|
@Input() fields: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
|
@Input() label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Configuration
|
||||||
|
*/
|
||||||
|
@Input() img: ImageField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether any valid HTTP(S) URL should be rendered as a link
|
||||||
|
*/
|
||||||
|
@Input() urlRegex?: string;
|
||||||
|
|
||||||
|
}
|
@@ -6,5 +6,6 @@
|
|||||||
[enableMarkdown]="enableMarkdown"
|
[enableMarkdown]="enableMarkdown"
|
||||||
[urlRegex]="urlRegex"
|
[urlRegex]="urlRegex"
|
||||||
[browseDefinition]="browseDefinition|async"
|
[browseDefinition]="browseDefinition|async"
|
||||||
|
[img]="img"
|
||||||
></ds-metadata-values>
|
></ds-metadata-values>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,6 +6,25 @@ import { BrowseDefinition } from '../../../../core/shared/browse-definition.mode
|
|||||||
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
|
||||||
import { getRemoteDataPayload } from '../../../../core/shared/operators';
|
import { getRemoteDataPayload } from '../../../../core/shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that encapsulate Image configuration for this component.
|
||||||
|
*/
|
||||||
|
export interface ImageField {
|
||||||
|
/**
|
||||||
|
* URI that is used to retrieve the image.
|
||||||
|
*/
|
||||||
|
URI: string;
|
||||||
|
/**
|
||||||
|
* i18n Key that represents the alt text to display
|
||||||
|
*/
|
||||||
|
alt: string;
|
||||||
|
/**
|
||||||
|
* CSS variable that contains the height of the inline image.
|
||||||
|
*/
|
||||||
|
heightVar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component can be used to represent metadata on a simple item page.
|
* This component can be used to represent metadata on a simple item page.
|
||||||
* It expects one input parameter of type Item to which the metadata belongs.
|
* It expects one input parameter of type Item to which the metadata belongs.
|
||||||
@@ -51,6 +70,11 @@ export class ItemPageFieldComponent {
|
|||||||
*/
|
*/
|
||||||
urlRegex?: string;
|
urlRegex?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Configuration
|
||||||
|
*/
|
||||||
|
img: ImageField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return browse definition that matches any field used in this component if it is configured as a browse
|
* Return browse definition that matches any field used in this component if it is configured as a browse
|
||||||
* link in dspace.cfg (webui.browse.link.<n>)
|
* link in dspace.cfg (webui.browse.link.<n>)
|
||||||
|
@@ -47,6 +47,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ExportBatchSelectorComponent
|
ExportBatchSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||||
|
import { PUBLICATION_CLAIMS_PATH } from './admin/admin-notifications/admin-notifications-routing-paths';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates all of the app's menus
|
* Creates all of the app's menus
|
||||||
@@ -559,6 +560,17 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
link: '/admin/notifications/quality-assurance'
|
link: '/admin/notifications/quality-assurance'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'notifications_publication-claim',
|
||||||
|
parentID: 'notifications',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.notifications_publication-claim',
|
||||||
|
link: '/admin/notifications/' + PUBLICATION_CLAIMS_PATH
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
/* Admin Search */
|
/* Admin Search */
|
||||||
{
|
{
|
||||||
id: 'admin_search',
|
id: 'admin_search',
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
|
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
|
||||||
|
<ds-suggestions-notification></ds-suggestions-notification>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-themed-search *ngIf="configuration && context"
|
<ds-themed-search *ngIf="configuration && context"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user