mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'DSpace:main' into main
This commit is contained in:
@@ -386,3 +386,9 @@ vocabularies:
|
||||
comcolSelectionSort:
|
||||
sortField: 'dc.title'
|
||||
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';
|
||||
|
||||
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
|
||||
export const PUBLICATION_CLAIMS_PATH = 'publication-claim';
|
||||
|
||||
export function getQualityAssuranceRoute(id: string) {
|
||||
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();
|
||||
|
@@ -4,6 +4,9 @@ import { RouterModule } from '@angular/router';
|
||||
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths';
|
||||
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
|
||||
import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service';
|
||||
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
|
||||
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
||||
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
||||
@@ -20,6 +23,21 @@ import {
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: `${PUBLICATION_CLAIMS_PATH}`,
|
||||
component: AdminNotificationsPublicationClaimPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notifications.publicationclaim.page.title',
|
||||
breadcrumbKey: 'admin.notifications.publicationclaim',
|
||||
showBreadcrumbsFluid: false
|
||||
}
|
||||
},
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
|
||||
@@ -71,7 +89,9 @@ import {
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService,
|
||||
AdminNotificationsPublicationClaimPageResolver,
|
||||
SourceDataResolver,
|
||||
AdminQualityAssuranceSourcePageResolver,
|
||||
AdminQualityAssuranceTopicsPageResolver,
|
||||
AdminQualityAssuranceEventsPageResolver,
|
||||
AdminQualityAssuranceSourcePageResolver,
|
||||
|
@@ -3,10 +3,11 @@ import { NgModule } from '@angular/core';
|
||||
import { CoreModule } from '../../core/core.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
|
||||
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
|
||||
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
||||
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
||||
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
|
||||
import {NotificationsModule} from '../../notifications/notifications.module';
|
||||
import { NotificationsModule } from '../../notifications/notifications.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -17,6 +18,7 @@ import {NotificationsModule} from '../../notifications/notifications.module';
|
||||
NotificationsModule
|
||||
],
|
||||
declarations: [
|
||||
AdminNotificationsPublicationClaimPageComponent,
|
||||
AdminQualityAssuranceTopicsPageComponent,
|
||||
AdminQualityAssuranceEventsPageComponent,
|
||||
AdminQualityAssuranceSourcePageComponent
|
||||
|
@@ -38,6 +38,7 @@ import {
|
||||
ThemedPageInternalServerErrorComponent
|
||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
|
||||
import { MenuResolver } from './menu.resolver';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
|
||||
@@ -206,6 +207,11 @@ import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-c
|
||||
.then((m) => m.ProcessPageModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{ path: SUGGESTION_MODULE_PATH,
|
||||
loadChildren: () => import('./suggestions-page/suggestions-page.module')
|
||||
.then((m) => m.SuggestionsPageModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{
|
||||
path: INFO_MODULE_PATH,
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
||||
|
@@ -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 { HALResource } from '../../shared/hal-resource.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { getLinkDefinition, link } from './build-decorators';
|
||||
import { dataService, getDataServiceFor, getLinkDefinition, link } from './build-decorators';
|
||||
|
||||
class TestHALResource implements HALResource {
|
||||
_links: {
|
||||
@@ -46,5 +46,17 @@ describe('build decorators', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`set data service`, () => {
|
||||
it(`should throw error`, () => {
|
||||
expect(dataService(null)).toThrow();
|
||||
});
|
||||
|
||||
it(`should set properly data service for type`, () => {
|
||||
const target = new TestHALResource();
|
||||
dataService(testType)(target);
|
||||
expect(getDataServiceFor(testType)).toEqual(target);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
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 { HALResource } from '../../shared/hal-resource.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { getResourceTypeValueFor } from '../object-cache.reducer';
|
||||
import {
|
||||
getResourceTypeValueFor
|
||||
} from '../object-cache.reducer';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { CacheableObject } from '../cacheable-object.model';
|
||||
import { TypedObject } from '../typed-object.model';
|
||||
|
||||
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<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', {
|
||||
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', {
|
||||
providedIn: 'root',
|
||||
@@ -20,6 +27,7 @@ const resolvedLinkKey = Symbol('resolvedLink');
|
||||
|
||||
const resolvedLinkMap = new Map();
|
||||
const typeMap = new Map();
|
||||
const dataServiceMap = new Map();
|
||||
const linkMap = new Map();
|
||||
|
||||
/**
|
||||
@@ -38,6 +46,39 @@ export function getClassForType(type: string | ResourceType) {
|
||||
return typeMap.get(getResourceTypeValueFor(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* A class decorator to indicate that this class is a dataservice
|
||||
* for a given resource type.
|
||||
*
|
||||
* "dataservice" in this context means that it has findByHref and
|
||||
* findAllByHref methods.
|
||||
*
|
||||
* @param resourceType the resource type the class is a dataservice for
|
||||
*/
|
||||
export function dataService(resourceType: ResourceType): any {
|
||||
return (target: any) => {
|
||||
if (hasNoValue(resourceType)) {
|
||||
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
|
||||
}
|
||||
const existingDataservice = dataServiceMap.get(resourceType.value);
|
||||
|
||||
if (hasValue(existingDataservice)) {
|
||||
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
|
||||
}
|
||||
|
||||
dataServiceMap.set(resourceType.value, target);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dataservice matching the given resource type
|
||||
*
|
||||
* @param resourceType the resource type you want the matching dataservice for
|
||||
*/
|
||||
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
|
||||
return dataServiceMap.get(resourceType.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to represent the data that can be set by the @link decorator
|
||||
*/
|
||||
@@ -65,7 +106,7 @@ export const link = <T extends HALResource>(
|
||||
resourceType: ResourceType,
|
||||
isList = false,
|
||||
linkName?: keyof T['_links'],
|
||||
) => {
|
||||
) => {
|
||||
return (target: T, propertyName: string) => {
|
||||
let targetMap = linkMap.get(target.constructor);
|
||||
|
||||
|
@@ -161,6 +161,7 @@ export class RemoteDataBuildService {
|
||||
} else {
|
||||
// in case the elements of the paginated list were already filled in, because they're UnCacheableObjects
|
||||
paginatedList.page = paginatedList.page
|
||||
.filter((obj: any) => obj != null)
|
||||
.map((obj: any) => this.plainObjectToInstance<T>(obj))
|
||||
.map((obj: any) =>
|
||||
this.linkService.resolveLinks(obj, ...pageLink.linksToFollow)
|
||||
|
@@ -185,6 +185,8 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
|
||||
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
|
||||
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
|
||||
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
|
||||
import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model';
|
||||
import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -386,7 +388,9 @@ export const models =
|
||||
IdentifierData,
|
||||
Subscription,
|
||||
ItemRequest,
|
||||
BulkAccessConditionOptions
|
||||
BulkAccessConditionOptions,
|
||||
SuggestionTarget,
|
||||
SuggestionSource
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -29,11 +29,11 @@ export const EMBED_SEPARATOR = '%2F';
|
||||
/**
|
||||
* Common functionality for data services.
|
||||
* Specific functionality that not all services would need
|
||||
* is implemented in "DataService feature" classes (e.g. {@link CreateData}
|
||||
* is implemented in "UpdateDataServiceImpl feature" classes (e.g. {@link CreateData}
|
||||
*
|
||||
* All DataService (or DataService feature) classes must
|
||||
* All UpdateDataServiceImpl (or UpdateDataServiceImpl feature) classes must
|
||||
* - extend this class (or {@link IdentifiableDataService})
|
||||
* - implement any DataService features it requires in order to forward calls to it
|
||||
* - implement any UpdateDataServiceImpl features it requires in order to forward calls to it
|
||||
*
|
||||
* ```
|
||||
* export class SomeDataService extends BaseDataService<Something> implements CreateData<Something>, SearchData<Something> {
|
||||
@@ -385,7 +385,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
||||
|
||||
/**
|
||||
* Return the links to traverse from the root of the api to the
|
||||
* endpoint this DataService represents
|
||||
* endpoint this UpdateDataServiceImpl represents
|
||||
*
|
||||
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
||||
* links to 'bar' the linkPath for the BarDataService would be
|
||||
|
@@ -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}
|
||||
* 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}
|
||||
* 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}
|
||||
* 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}
|
||||
* 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}
|
||||
* and delegating its method to an inner instance of this class.
|
||||
|
@@ -17,8 +17,8 @@ import { HALDataService } from './base/hal-data-service.interface';
|
||||
import { dataService } from './base/data-service.decorator';
|
||||
|
||||
/**
|
||||
* A DataService with only findByHref methods. Its purpose is to be used for resources that don't
|
||||
* need to be retrieved by ID, or have any way to update them, but require a DataService in order
|
||||
* A UpdateDataServiceImpl with only findByHref methods. Its purpose is to be used for resources that don't
|
||||
* need to be retrieved by ID, or have any way to update them, but require a UpdateDataServiceImpl in order
|
||||
* for their links to be resolved by the LinkService.
|
||||
*
|
||||
* an @dataService annotation can be added for any number of these resource types
|
||||
|
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 { 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> {
|
||||
patch(dso: T, operations: Operation[]): 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', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
|
||||
scheduler.flush();
|
||||
|
||||
|
@@ -315,7 +315,7 @@ describe('EPersonDataService', () => {
|
||||
service.deleteEPerson(EPersonMock).subscribe();
|
||||
});
|
||||
|
||||
it('should call DataService.delete with the EPerson\'s UUID', () => {
|
||||
it('should call UpdateDataServiceImpl.delete with the EPerson\'s UUID', () => {
|
||||
expect(service.delete).toHaveBeenCalledWith(EPersonMock.id);
|
||||
});
|
||||
});
|
||||
|
@@ -27,6 +27,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
||||
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
|
||||
|
@@ -126,7 +126,7 @@ describe('WorkflowItemDataService test', () => {
|
||||
});
|
||||
|
||||
describe('findByItem', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||
scheduler.flush();
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
@@ -21,6 +21,11 @@ import { RequestEntry } from '../data/request-entry.model';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { testSearchDataImplementation } from '../data/base/search-data.spec';
|
||||
import { testDeleteDataImplementation } from '../data/base/delete-data.spec';
|
||||
import { SearchData } from '../data/base/search-data';
|
||||
import { DeleteData } from '../data/base/delete-data';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
||||
describe('WorkspaceitemDataService test', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -68,15 +73,12 @@ describe('WorkspaceitemDataService test', () => {
|
||||
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
||||
const searchRequestURL$ = observableOf(searchRequestURL);
|
||||
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const comparatorEntry = {} as any;
|
||||
const store = {} as Store<CoreState>;
|
||||
const pageInfo = new PageInfo();
|
||||
@@ -84,18 +86,23 @@ describe('WorkspaceitemDataService test', () => {
|
||||
function initTestService() {
|
||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||
return new WorkspaceitemDataService(
|
||||
comparatorEntry,
|
||||
halService,
|
||||
http,
|
||||
notificationsService,
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
store
|
||||
);
|
||||
}
|
||||
|
||||
describe('composition', () => {
|
||||
const initService = () => new WorkspaceitemDataService(null, null, null, null, null);
|
||||
testSearchDataImplementation(initService);
|
||||
testDeleteDataImplementation(initService);
|
||||
const initSearchService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as SearchData<any>;
|
||||
const initDeleteService = () => new WorkspaceitemDataService(null, null, null, null, null, null, null, null) as unknown as DeleteData<any>;
|
||||
|
||||
testSearchDataImplementation(initSearchService);
|
||||
testDeleteDataImplementation(initDeleteService);
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
@@ -104,7 +111,7 @@ describe('WorkspaceitemDataService test', () => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: endpointURL })
|
||||
getEndpoint: observableOf(endpointURL)
|
||||
});
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
@@ -120,13 +127,13 @@ describe('WorkspaceitemDataService test', () => {
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: hot('a|', {
|
||||
a: wsiRD
|
||||
})
|
||||
}),
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
|
||||
spyOn((service as any), 'findByHref').and.callThrough();
|
||||
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -134,11 +141,11 @@ describe('WorkspaceitemDataService test', () => {
|
||||
});
|
||||
|
||||
describe('findByItem', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
||||
const searchUrl = service.getIDHref('item', [new RequestParam('uuid', encodeURIComponent('1234-1234'))]);
|
||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchUrl, true, true);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<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 {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import {HttpOptions} from '../dspace-rest/dspace-rest.service';
|
||||
import {find, map} from 'rxjs/operators';
|
||||
import {PostRequest} from '../data/request.models';
|
||||
import {hasValue} from '../../shared/empty.util';
|
||||
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { dataService } from '../data/base/data-service.decorator';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(WorkspaceItem.type)
|
||||
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements SearchData<WorkspaceItem>, DeleteData<WorkspaceItem> {
|
||||
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements DeleteData<WorkspaceItem>, SearchData<WorkspaceItem>{
|
||||
protected linkPath = 'workspaceitems';
|
||||
protected searchByItemLinkPath = 'item';
|
||||
|
||||
private searchData: SearchDataImpl<WorkspaceItem>;
|
||||
private deleteData: DeleteDataImpl<WorkspaceItem>;
|
||||
private deleteData: DeleteData<WorkspaceItem>;
|
||||
private searchData: SearchData<WorkspaceItem>;
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||
protected halService: HALEndpointService,
|
||||
protected http: HttpClient,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
) {
|
||||
protected store: Store<CoreState>) {
|
||||
super('workspaceitems', requestService, rdbService, objectCache, halService);
|
||||
|
||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||
}
|
||||
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||
return this.deleteData.delete(objectId, copyVirtualMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the WorkspaceItem object found through the UUID of an item
|
||||
*
|
||||
@@ -55,21 +67,46 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
|
||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||
const findListOptions = new FindListOptions();
|
||||
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||
const href$ = this.getIDHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the HREF for a specific object's search method with given options object
|
||||
*
|
||||
* @param searchMethod The search method for the object
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
* Import an external source entry into a collection
|
||||
* @param externalSourceEntryHref
|
||||
* @param collectionId
|
||||
*/
|
||||
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow): Observable<string> {
|
||||
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||
public importExternalSourceEntry(externalSourceEntryHref: string, collectionId: string): Observable<RemoteData<WorkspaceItem>> {
|
||||
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 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
} from '../../core/shared/operators';
|
||||
import { UpdateDataService } from '../../core/data/update-data.service';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -22,6 +21,7 @@ import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analy
|
||||
import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { HALDataService } from '../../core/data/base/hal-data-service.interface';
|
||||
import { UpdateDataService } from '../../core/data/update-data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-edit-metadata',
|
||||
|
@@ -7,3 +7,4 @@
|
||||
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
||||
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
||||
</div>
|
||||
<ds-suggestions-popup></ds-suggestions-popup>
|
||||
|
@@ -13,6 +13,7 @@ import { RecentItemListComponent } from './recent-item-list/recent-item-list.com
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
const DECLARATIONS = [
|
||||
HomePageComponent,
|
||||
@@ -31,7 +32,8 @@ const DECLARATIONS = [
|
||||
JournalEntitiesModule.withEntryComponents(),
|
||||
ResearchEntitiesModule.withEntryComponents(),
|
||||
HomePageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
StatisticsModule.forRoot(),
|
||||
NotificationsModule
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS,
|
||||
|
@@ -47,6 +47,7 @@ import {
|
||||
import {
|
||||
ExportBatchSelectorComponent
|
||||
} 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
|
||||
@@ -560,6 +561,17 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
link: '/admin/notifications/quality-assurance'
|
||||
} 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 */
|
||||
{
|
||||
id: 'admin_search',
|
||||
|
@@ -1,5 +1,6 @@
|
||||
<div class="container">
|
||||
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
|
||||
<ds-suggestions-notification></ds-suggestions-notification>
|
||||
</div>
|
||||
|
||||
<ds-themed-search *ngIf="configuration && context"
|
||||
|
@@ -15,6 +15,7 @@ import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission
|
||||
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
|
||||
import { SearchModule } from '../shared/search/search.module';
|
||||
import { UploadModule } from '../shared/upload/upload.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
const DECLARATIONS = [
|
||||
MyDSpacePageComponent,
|
||||
@@ -33,6 +34,7 @@ const DECLARATIONS = [
|
||||
MyDspacePageRoutingModule,
|
||||
MyDspaceSearchModule.withEntryComponents(),
|
||||
UploadModule,
|
||||
NotificationsModule
|
||||
],
|
||||
declarations: DECLARATIONS,
|
||||
providers: [
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { QualityAssuranceSourceEffects } from './qa/source/quality-assurance-source.effects';
|
||||
import { QualityAssuranceTopicsEffects } from './qa/topics/quality-assurance-topics.effects';
|
||||
import { SuggestionTargetsEffects } from '../suggestion-notifications/suggestion-targets/suggestion-targets.effects';
|
||||
|
||||
export const notificationsEffects = [
|
||||
QualityAssuranceTopicsEffects,
|
||||
QualityAssuranceSourceEffects
|
||||
QualityAssuranceSourceEffects,
|
||||
SuggestionTargetsEffects
|
||||
];
|
||||
|
@@ -26,6 +26,30 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou
|
||||
import {
|
||||
QualityAssuranceSourceDataService
|
||||
} from '../core/notifications/qa/source/quality-assurance-source-data.service';
|
||||
import { PublicationClaimComponent } from '../suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component';
|
||||
import { SuggestionActionsComponent } from '../suggestion-notifications/suggestion-actions/suggestion-actions.component';
|
||||
import {
|
||||
SuggestionListElementComponent
|
||||
} from '../suggestion-notifications/suggestion-list-element/suggestion-list-element.component';
|
||||
import {
|
||||
SuggestionEvidencesComponent
|
||||
} from '../suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component';
|
||||
import { SuggestionsPopupComponent } from '../suggestion-notifications/suggestions-popup/suggestions-popup.component';
|
||||
import {
|
||||
SuggestionsNotificationComponent
|
||||
} from '../suggestion-notifications/suggestions-notification/suggestions-notification.component';
|
||||
import { SuggestionsService } from '../suggestion-notifications/suggestions.service';
|
||||
import { SuggestionsDataService } from '../core/suggestion-notifications/suggestions-data.service';
|
||||
import {
|
||||
SuggestionSourceDataService
|
||||
} from '../core/suggestion-notifications/source/suggestion-source-data.service';
|
||||
import {
|
||||
SuggestionTargetDataService
|
||||
} from '../core/suggestion-notifications/target/suggestion-target-data.service';
|
||||
import {
|
||||
SuggestionTargetsStateService
|
||||
} from '../suggestion-notifications/suggestion-targets/suggestion-targets.state.service';
|
||||
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -40,7 +64,13 @@ const MODULES = [
|
||||
const COMPONENTS = [
|
||||
QualityAssuranceTopicsComponent,
|
||||
QualityAssuranceEventsComponent,
|
||||
QualityAssuranceSourceComponent
|
||||
QualityAssuranceSourceComponent,
|
||||
PublicationClaimComponent,
|
||||
SuggestionActionsComponent,
|
||||
SuggestionListElementComponent,
|
||||
SuggestionEvidencesComponent,
|
||||
SuggestionsPopupComponent,
|
||||
SuggestionsNotificationComponent
|
||||
];
|
||||
|
||||
const DIRECTIVES = [ ];
|
||||
@@ -55,7 +85,12 @@ const PROVIDERS = [
|
||||
QualityAssuranceSourceService,
|
||||
QualityAssuranceTopicDataService,
|
||||
QualityAssuranceSourceDataService,
|
||||
QualityAssuranceEventDataService
|
||||
QualityAssuranceEventDataService,
|
||||
SuggestionsService,
|
||||
SuggestionSourceDataService,
|
||||
SuggestionTargetDataService,
|
||||
SuggestionTargetsStateService,
|
||||
SuggestionsDataService
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -1,12 +1,7 @@
|
||||
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
|
||||
import {
|
||||
qualityAssuranceSourceReducer,
|
||||
QualityAssuranceSourceState
|
||||
} from './qa/source/quality-assurance-source.reducer';
|
||||
import {
|
||||
qualityAssuranceTopicsReducer,
|
||||
QualityAssuranceTopicState,
|
||||
} from './qa/topics/quality-assurance-topics.reducer';
|
||||
import { qualityAssuranceSourceReducer, QualityAssuranceSourceState } from './qa/source/quality-assurance-source.reducer';
|
||||
import { qualityAssuranceTopicsReducer, QualityAssuranceTopicState, } from './qa/topics/quality-assurance-topics.reducer';
|
||||
import { SuggestionTargetsReducer, SuggestionTargetState } from '../suggestion-notifications/suggestion-targets/suggestion-targets.reducer';
|
||||
|
||||
/**
|
||||
* The OpenAIRE State
|
||||
@@ -14,11 +9,13 @@ import {
|
||||
export interface SuggestionNotificationsState {
|
||||
'qaTopic': QualityAssuranceTopicState;
|
||||
'qaSource': QualityAssuranceSourceState;
|
||||
'suggestionTarget': SuggestionTargetState;
|
||||
}
|
||||
|
||||
export const suggestionNotificationsReducers: ActionReducerMap<SuggestionNotificationsState> = {
|
||||
qaTopic: qualityAssuranceTopicsReducer,
|
||||
qaSource: qualityAssuranceSourceReducer
|
||||
qaSource: qualityAssuranceSourceReducer,
|
||||
suggestionTarget: SuggestionTargetsReducer
|
||||
};
|
||||
|
||||
export const suggestionNotificationsSelector = createFeatureSelector<SuggestionNotificationsState>('suggestionNotifications');
|
||||
|
@@ -8,6 +8,7 @@
|
||||
<div class="mb-4">
|
||||
<ds-profile-page-researcher-form [user]="user" ></ds-profile-page-researcher-form>
|
||||
</div>
|
||||
<ds-suggestions-notification></ds-suggestions-notification>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -12,7 +12,7 @@ import { ThemedProfilePageComponent } from './themed-profile-page.component';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { UiSwitchModule } from 'ngx-ui-switch';
|
||||
import { ProfileClaimItemModalComponent } from './profile-claim-item-modal/profile-claim-item-modal.component';
|
||||
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -20,7 +20,8 @@ import { ProfileClaimItemModalComponent } from './profile-claim-item-modal/profi
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
FormModule,
|
||||
UiSwitchModule
|
||||
UiSwitchModule,
|
||||
NotificationsModule
|
||||
],
|
||||
exports: [
|
||||
ProfilePageComponent,
|
||||
|
@@ -5,13 +5,14 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { DSOSelectorModalWrapperComponent, SelectorActionType } from './dso-selector-modal-wrapper.component';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DSOSelectorComponent } from '../dso-selector/dso-selector.component';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { createSuccessfulRemoteDataObject } from '../../remote-data.utils';
|
||||
import { hasValue } from '../../empty.util';
|
||||
|
||||
describe('DSOSelectorModalWrapperComponent', () => {
|
||||
let component: DSOSelectorModalWrapperComponent;
|
||||
@@ -83,6 +84,20 @@ describe('DSOSelectorModalWrapperComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectObject with emit only', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component, 'navigate');
|
||||
spyOn(component, 'close');
|
||||
spyOn(component.select, 'emit');
|
||||
component.emitOnly = true;
|
||||
component.selectObject(item);
|
||||
});
|
||||
it('should call the close and navigate method on the component with the given DSO', () => {
|
||||
expect(component.close).toHaveBeenCalled();
|
||||
expect(component.select.emit).toHaveBeenCalledWith(item);
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
beforeEach(() => {
|
||||
component.close();
|
||||
@@ -113,6 +128,19 @@ describe('DSOSelectorModalWrapperComponent', () => {
|
||||
expect(component.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should find route data', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component, 'findRouteData');
|
||||
component.ngOnInit();
|
||||
});
|
||||
it('should call the findRouteData method on the component', () => {
|
||||
expect(component.findRouteData).toHaveBeenCalled();
|
||||
});
|
||||
it('should return undefined', () => {
|
||||
expect(component.findRouteData((route) => hasValue(route.data), {} as unknown as ActivatedRouteSnapshot)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
@@ -29,6 +29,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
||||
*/
|
||||
@Input() dsoRD: RemoteData<DSpaceObject>;
|
||||
|
||||
/**
|
||||
* Representing if component should emit value of selected entries or navigate
|
||||
*/
|
||||
@Input() emitOnly = false;
|
||||
|
||||
/**
|
||||
* Optional header to display above the selection list
|
||||
* Supports i18n keys
|
||||
@@ -50,6 +55,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
||||
*/
|
||||
action: SelectorActionType;
|
||||
|
||||
/**
|
||||
* Event emitted when a DSO entry is selected if emitOnly is set to true
|
||||
*/
|
||||
@Output() select: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
|
||||
|
||||
/**
|
||||
* Default DSO ordering
|
||||
*/
|
||||
@@ -93,8 +103,12 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
||||
*/
|
||||
selectObject(dso: DSpaceObject) {
|
||||
this.close();
|
||||
if (this.emitOnly) {
|
||||
this.select.emit(dso);
|
||||
} else {
|
||||
this.navigate(dso);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a page based on the DSpaceObject provided
|
||||
|
@@ -96,7 +96,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
|
||||
private pageConfigSub: Subscription;
|
||||
|
||||
/**
|
||||
* Initialize instance variables and inject the properly DataService
|
||||
* Initialize instance variables and inject the properly UpdateDataServiceImpl
|
||||
*
|
||||
* @param {DSONameService} dsoNameService
|
||||
* @param {Injector} parentInjector
|
||||
|
@@ -54,4 +54,31 @@ describe('FilterInputSuggestionsComponent', () => {
|
||||
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('component methods', () => {
|
||||
const testData = {
|
||||
value: 'test-field'
|
||||
} as unknown as any;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(comp.submitSuggestion, 'emit');
|
||||
spyOn(comp.clickSuggestion, 'emit');
|
||||
spyOn(comp, 'close');
|
||||
});
|
||||
|
||||
it('should properly submit', () => {
|
||||
comp.onSubmit(testData);
|
||||
expect(comp.submitSuggestion.emit).toHaveBeenCalledWith(testData);
|
||||
expect(comp.value).toBe(testData);
|
||||
});
|
||||
|
||||
it('should update value on suggestion clicked', () => {
|
||||
comp.onClickSuggestion(testData);
|
||||
expect(comp.clickSuggestion.emit).toHaveBeenCalledWith(testData);
|
||||
expect(comp.value).toBe(testData.value);
|
||||
expect(comp.blockReopen).toBeTruthy();
|
||||
expect(comp.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
42
src/app/shared/mocks/publication-claim-targets.mock.ts
Normal file
42
src/app/shared/mocks/publication-claim-targets.mock.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
|
||||
// REST Mock ---------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
export const mockSuggestionTargetsObjectOne: SuggestionTarget = {
|
||||
type: new ResourceType('suggestiontarget'),
|
||||
id: 'reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26',
|
||||
display: 'Bollini, Andrea',
|
||||
source: 'reciter',
|
||||
total: 31,
|
||||
_links: {
|
||||
target: {
|
||||
href: 'https://rest.api/rest/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26'
|
||||
},
|
||||
suggestions: {
|
||||
href: 'https://rest.api/rest/api/integration/suggestions/search/findByTargetAndSource?target=gf3d657-9d6d-4a87-b905-fef0f8cae26c&source=reciter'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/integration/suggestiontargets/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mockSuggestionTargetsObjectTwo: SuggestionTarget = {
|
||||
type: new ResourceType('suggestiontarget'),
|
||||
id: 'reciter:nhy567-9d6d-ty67-b905-fef0f8cae26',
|
||||
display: 'Digilio, Andrea',
|
||||
source: 'reciter',
|
||||
total: 12,
|
||||
_links: {
|
||||
target: {
|
||||
href: 'https://rest.api/rest/api/core/items/nhy567-9d6d-ty67-b905-fef0f8cae26'
|
||||
},
|
||||
suggestions: {
|
||||
href: 'https://rest.api/rest/api/integration/suggestions/search/findByTargetAndSource?target=nhy567-9d6d-ty67-b905-fef0f8cae26&source=reciter'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/integration/suggestiontargets/reciter:nhy567-9d6d-ty67-b905-fef0f8cae26'
|
||||
}
|
||||
}
|
||||
};
|
210
src/app/shared/mocks/publication-claim.mock.ts
Normal file
210
src/app/shared/mocks/publication-claim.mock.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
// REST Mock ---------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model';
|
||||
import { SUGGESTION } from '../../core/suggestion-notifications/models/suggestion-objects.resource-type';
|
||||
|
||||
export const mockSuggestionPublicationOne: Suggestion = {
|
||||
id: '24694773',
|
||||
display: 'publication one',
|
||||
source: 'reciter',
|
||||
externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772',
|
||||
score: '48',
|
||||
evidences: {
|
||||
acceptedRejectedEvidence: {
|
||||
score: '2.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
authorNameEvidence: {
|
||||
score: '0',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
journalCategoryEvidence: {
|
||||
score: '6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
affiliationEvidence: {
|
||||
score: 'xxx',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
relationshipEvidence: {
|
||||
score: '9',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
educationYearEvidence: {
|
||||
score: '3.6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
personTypeEvidence: {
|
||||
score: '4',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
articleCountEvidence: {
|
||||
score: '6.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
averageClusteringEvidence: {
|
||||
score: '7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: 'https://publication/0000-0003-3681-2038',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1,
|
||||
place: -1
|
||||
} as any
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'publication one',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
value: '2010-11-03',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dspace.entity.type': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: 'OrgUnit',
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.description': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).",
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
]
|
||||
},
|
||||
type: SUGGESTION,
|
||||
_links: {
|
||||
target: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/suggestions/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26c:24694772'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mockSuggestionPublicationTwo: Suggestion = {
|
||||
id: '24694772',
|
||||
display: 'publication two',
|
||||
source: 'reciter',
|
||||
externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772',
|
||||
score: '48',
|
||||
evidences: {
|
||||
acceptedRejectedEvidence: {
|
||||
score: '2.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
authorNameEvidence: {
|
||||
score: '0',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
journalCategoryEvidence: {
|
||||
score: '6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
affiliationEvidence: {
|
||||
score: 'xxx',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
relationshipEvidence: {
|
||||
score: '9',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
educationYearEvidence: {
|
||||
score: '3.6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
personTypeEvidence: {
|
||||
score: '4',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
articleCountEvidence: {
|
||||
score: '6.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
averageClusteringEvidence: {
|
||||
score: '7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: 'https://publication/0000-0003-3681-2038',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1,
|
||||
place: -1
|
||||
} as any
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'publication one',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
value: '2010-11-03',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dspace.entity.type': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: 'OrgUnit',
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.description': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).",
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
]
|
||||
},
|
||||
type: SUGGESTION,
|
||||
_links: {
|
||||
target: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/suggestions/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26c:24694772'
|
||||
}
|
||||
}
|
||||
};
|
1360
src/app/shared/mocks/suggestion.mock.ts
Normal file
1360
src/app/shared/mocks/suggestion.mock.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import { CacheableObject } from '../../core/cache/cacheable-object.model';
|
||||
import { IdentifiableDataService } from '../../core/data/base/identifiable-data.service';
|
||||
|
||||
/**
|
||||
* Class to return DataService for given ResourceType
|
||||
* Class to return UpdateDataServiceImpl for given ResourceType
|
||||
*/
|
||||
export class MyDSpaceActionsServiceFactory<T extends CacheableObject, TService extends IdentifiableDataService<T>> {
|
||||
public getConstructor(type: ResourceType): TService {
|
||||
|
@@ -47,7 +47,7 @@ export abstract class MyDSpaceActionsComponent<T extends DSpaceObject, TService
|
||||
public processing$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Instance of DataService related to mydspace object
|
||||
* Instance of UpdateDataServiceImpl related to mydspace object
|
||||
*/
|
||||
protected objectDataService: TService;
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { FilterType } from '../../../models/filter-type.model';
|
||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER
|
||||
} from '../../../../../core/shared/search/search-filter.service';
|
||||
@@ -35,6 +36,11 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
||||
*/
|
||||
@Input() refreshFilters: BehaviorSubject<boolean>;
|
||||
|
||||
/**
|
||||
* The current scope
|
||||
*/
|
||||
@Input() scope: string;
|
||||
|
||||
/**
|
||||
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
||||
*/
|
||||
@@ -56,7 +62,8 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
||||
providers: [
|
||||
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] },
|
||||
{ provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] },
|
||||
{ provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] }
|
||||
{ provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] },
|
||||
{ provide: SCOPE, useFactory: () => (this.scope), deps: [] },
|
||||
],
|
||||
parent: this.injector
|
||||
});
|
||||
|
@@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER,
|
||||
SearchFilterService
|
||||
@@ -99,6 +100,7 @@ describe('SearchFacetFilterComponent', () => {
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
||||
{ provide: SCOPE, useValue: undefined },
|
||||
{
|
||||
provide: SearchFilterService, useValue: {
|
||||
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
||||
|
@@ -22,6 +22,7 @@ import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER,
|
||||
SearchFilterService
|
||||
@@ -104,7 +105,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>) {
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>,
|
||||
@Inject(SCOPE) public scope: string,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,8 +117,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
this.currentUrl = this.router.url;
|
||||
this.filterValues$ = new BehaviorSubject(createPendingRemoteDataObject());
|
||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||
|
||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||
this.searchOptions$ = this.searchConfigService.searchOptions.pipe(
|
||||
map((options: SearchOptions) => hasNoValue(this.scope) ? options : Object.assign({}, options, {
|
||||
scope: this.scope,
|
||||
})),
|
||||
);
|
||||
this.subs.push(
|
||||
this.searchOptions$.subscribe(() => this.updateFilterValueList()),
|
||||
this.refreshFilters.asObservable().pipe(
|
||||
|
@@ -19,6 +19,7 @@
|
||||
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
|
||||
class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }">
|
||||
<ds-search-facet-filter-wrapper
|
||||
[scope]="scope"
|
||||
[filterConfig]="filter"
|
||||
[inPlaceSearch]="inPlaceSearch"
|
||||
[refreshFilters]="refreshFilters" >
|
||||
|
@@ -6,7 +6,7 @@ import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { SearchFilterConfig } from '../../models/search-filter-config.model';
|
||||
import { SearchFilterService } from '../../../../core/shared/search/search-filter.service';
|
||||
import { slide } from '../../../animations/slide';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { isNotEmpty, hasValue } from '../../../empty.util';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
|
||||
@@ -38,6 +38,11 @@ export class SearchFilterComponent implements OnInit {
|
||||
*/
|
||||
@Input() refreshFilters: BehaviorSubject<boolean>;
|
||||
|
||||
/**
|
||||
* The current scope
|
||||
*/
|
||||
@Input() scope: string;
|
||||
|
||||
/**
|
||||
* True when the filter is 100% collapsed in the UI
|
||||
*/
|
||||
@@ -171,6 +176,9 @@ export class SearchFilterComponent implements OnInit {
|
||||
} else {
|
||||
return this.searchConfigService.searchOptions.pipe(
|
||||
switchMap((options) => {
|
||||
if (hasValue(this.scope)) {
|
||||
options.scope = this.scope;
|
||||
}
|
||||
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
|
||||
filter((RD) => !RD.isLoading),
|
||||
map((valuesRD) => {
|
||||
|
@@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
SearchFilterService,
|
||||
REFRESH_FILTER
|
||||
@@ -75,7 +76,8 @@ describe('SearchHierarchyFilterComponent', () => {
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||
{ provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) },
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false)}
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false)},
|
||||
{ provide: SCOPE, useValue: undefined },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
SearchFilterService, REFRESH_FILTER
|
||||
} from '../../../../../core/shared/search/search-filter.service';
|
||||
@@ -49,9 +50,10 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>,
|
||||
@Inject(SCOPE) public scope: string,
|
||||
) {
|
||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
|
||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, scope);
|
||||
}
|
||||
|
||||
vocabularyExists$: Observable<boolean>;
|
||||
|
@@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER,
|
||||
SearchFilterService
|
||||
@@ -105,6 +106,7 @@ describe('SearchRangeFilterComponent', () => {
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
||||
{ provide: SCOPE, useValue: undefined },
|
||||
{
|
||||
provide: SearchFilterService, useValue: {
|
||||
getSelectedValuesForFilter: () => selectedValues,
|
||||
|
@@ -10,6 +10,7 @@ import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/se
|
||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||
import {
|
||||
FILTER_CONFIG,
|
||||
SCOPE,
|
||||
IN_PLACE_SEARCH,
|
||||
REFRESH_FILTER,
|
||||
SearchFilterService
|
||||
@@ -101,8 +102,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>,
|
||||
@Inject(SCOPE) public scope: string,
|
||||
private route: RouteService) {
|
||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
|
||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters, scope);
|
||||
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<h3>{{"search.filters.head" | translate}}</h3>
|
||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||
<ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
||||
<ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { SearchSettingsComponent } from './search-settings.component';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
@@ -6,15 +5,11 @@ import { PaginationComponentOptions } from '../../pagination/pagination-componen
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||
import { VarDirective } from '../../utils/var.directive';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { SidebarService } from '../../sidebar/sidebar.service';
|
||||
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../testing/pagination-service.stub';
|
||||
|
||||
@@ -22,32 +17,23 @@ describe('SearchSettingsComponent', () => {
|
||||
|
||||
let comp: SearchSettingsComponent;
|
||||
let fixture: ComponentFixture<SearchSettingsComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
|
||||
let pagination: PaginationComponentOptions;
|
||||
let sort: SortOptions;
|
||||
let mockResults;
|
||||
let searchServiceStub;
|
||||
|
||||
let queryParam;
|
||||
let scopeParam;
|
||||
let paginatedSearchOptions;
|
||||
|
||||
let paginationService;
|
||||
let paginationService: PaginationServiceStub;
|
||||
|
||||
let activatedRouteStub;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(waitForAsync(async () => {
|
||||
pagination = new PaginationComponentOptions();
|
||||
pagination.id = 'search-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
sort = new SortOptions('score', SortDirection.DESC);
|
||||
mockResults = ['test', 'data'];
|
||||
searchServiceStub = {
|
||||
searchOptions: { pagination: pagination, sort: sort },
|
||||
search: () => mockResults,
|
||||
};
|
||||
|
||||
queryParam = 'test query';
|
||||
scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
@@ -58,30 +44,12 @@ describe('SearchSettingsComponent', () => {
|
||||
sort,
|
||||
};
|
||||
|
||||
activatedRouteStub = {
|
||||
queryParams: observableOf({
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
}),
|
||||
};
|
||||
|
||||
paginationService = new PaginationServiceStub(pagination, sort);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{
|
||||
provide: SidebarService,
|
||||
useValue: SidebarServiceStub,
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: PaginationService,
|
||||
useValue: paginationService,
|
||||
@@ -111,10 +79,7 @@ describe('SearchSettingsComponent', () => {
|
||||
|
||||
// SearchPageComponent test instance
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
||||
spyOn(comp, 'reloadOrder');
|
||||
spyOn(searchServiceObject, 'search').and.callThrough();
|
||||
|
||||
});
|
||||
|
||||
it('it should show the order settings with the respective selectable options', () => {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { Component, Inject, Input } from '@angular/core';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
@@ -26,11 +24,10 @@ export class SearchSettingsComponent {
|
||||
*/
|
||||
@Input() sortOptionsList: SortOptions[];
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
||||
constructor(
|
||||
protected paginationService: PaginationService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -36,8 +36,6 @@ import { getCollectionPageRoute } from '../../collection-page/collection-page-ro
|
||||
|
||||
let comp: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||
const store: Store<SearchComponent> = jasmine.createSpyObj('store', {
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
dispatch: {},
|
||||
@@ -93,7 +91,6 @@ const mockDso2 = Object.assign(new Item(), {
|
||||
}
|
||||
}
|
||||
});
|
||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||
const mockSearchResults: SearchObjects<DSpaceObject> = Object.assign(new SearchObjects(), {
|
||||
page: [mockDso, mockDso2]
|
||||
});
|
||||
@@ -106,23 +103,13 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||
getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig),
|
||||
trackSearch: {},
|
||||
}) as SearchService;
|
||||
const configurationParam = 'default';
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const fixedFilter = 'fixed filter';
|
||||
|
||||
const defaultSearchOptions = new PaginatedSearchOptions({ pagination });
|
||||
|
||||
const paginatedSearchOptions$ = new BehaviorSubject(defaultSearchOptions);
|
||||
|
||||
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||
configuration: configurationParam,
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
fixedFilter: fixedFilter,
|
||||
pagination,
|
||||
sort
|
||||
});
|
||||
const activatedRouteStub = {
|
||||
snapshot: {
|
||||
queryParamMap: new Map([
|
||||
@@ -155,14 +142,11 @@ const filtersConfigRD = createSuccessfulRemoteDataObject([mockFilterConfig, mock
|
||||
const filtersConfigRD$ = observableOf(filtersConfigRD);
|
||||
|
||||
const routeServiceStub = {
|
||||
getRouteParameterValue: () => {
|
||||
return observableOf('');
|
||||
},
|
||||
getQueryParameterValue: () => {
|
||||
return observableOf('');
|
||||
return observableOf(null);
|
||||
},
|
||||
getQueryParamsWithPrefix: () => {
|
||||
return observableOf('');
|
||||
return observableOf(null);
|
||||
},
|
||||
setParameter: () => {
|
||||
return;
|
||||
@@ -252,16 +236,10 @@ describe('SearchComponent', () => {
|
||||
comp.paginationId = paginationId;
|
||||
|
||||
spyOn((comp as any), 'getSearchOptions').and.returnValue(paginatedSearchOptions$.asObservable());
|
||||
|
||||
searchServiceObject = TestBed.inject(SearchService);
|
||||
searchConfigurationServiceObject = TestBed.inject(SEARCH_CONFIG_SERVICE);
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
comp = null;
|
||||
searchServiceObject = null;
|
||||
searchConfigurationServiceObject = null;
|
||||
});
|
||||
|
||||
it('should init search parameters properly and call retrieveSearchResults', fakeAsync(() => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output, OnDestroy } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
@@ -11,7 +11,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../animations/push';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { SidebarService } from '../sidebar/sidebar.service';
|
||||
import { hasValue, hasValueOperator, isNotEmpty } from '../empty.util';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../empty.util';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
|
||||
import { PaginatedSearchOptions } from './models/paginated-search-options.model';
|
||||
@@ -34,7 +34,7 @@ import { CollectionElementLinkType } from '../object-collection/collection-eleme
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { SubmissionObject } from '../../core/submission/models/submission-object.model';
|
||||
import { SearchFilterConfig } from './models/search-filter-config.model';
|
||||
import { WorkspaceItem } from '../..//core/submission/models/workspaceitem.model';
|
||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
||||
import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths';
|
||||
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
|
||||
@@ -50,7 +50,7 @@ import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routi
|
||||
/**
|
||||
* This component renders a sidebar, a search input bar and the search results.
|
||||
*/
|
||||
export class SearchComponent implements OnInit {
|
||||
export class SearchComponent implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* The list of available configuration options
|
||||
@@ -166,6 +166,11 @@ export class SearchComponent implements OnInit {
|
||||
*/
|
||||
@Input() query: string;
|
||||
|
||||
/**
|
||||
* The fallback scope when no scope is defined in the url, if this is also undefined no scope will be set
|
||||
*/
|
||||
@Input() scope: string;
|
||||
|
||||
/**
|
||||
* The current configuration used during the search
|
||||
*/
|
||||
@@ -179,7 +184,7 @@ export class SearchComponent implements OnInit {
|
||||
/**
|
||||
* The current sort options used
|
||||
*/
|
||||
currentScope$: BehaviorSubject<string> = new BehaviorSubject<string>('');
|
||||
currentScope$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The current sort options used
|
||||
@@ -299,6 +304,10 @@ export class SearchComponent implements OnInit {
|
||||
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
||||
}
|
||||
|
||||
this.currentScope$ = this.routeService.getQueryParameterValue('scope').pipe(
|
||||
map((routeValue: string) => hasValue(routeValue) ? routeValue : this.scope),
|
||||
);
|
||||
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.currentContext$.next(this.context);
|
||||
@@ -306,9 +315,8 @@ export class SearchComponent implements OnInit {
|
||||
// Determinate PaginatedSearchOptions and listen to any update on it
|
||||
const configuration$: Observable<string> = this.searchConfigService
|
||||
.getCurrentConfiguration(this.configuration).pipe(distinctUntilChanged());
|
||||
const searchSortOptions$: Observable<SortOptions[]> = configuration$.pipe(
|
||||
switchMap((configuration: string) => this.searchConfigService
|
||||
.getConfigurationSearchConfig(configuration)),
|
||||
const searchSortOptions$: Observable<SortOptions[]> = combineLatest([configuration$, this.currentScope$]).pipe(
|
||||
switchMap(([configuration, scope]: [string, string]) => this.searchConfigService.getConfigurationSearchConfig(configuration, scope)),
|
||||
map((searchConfig: SearchConfig) => this.searchConfigService.getConfigurationSortOptions(searchConfig)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
@@ -321,13 +329,13 @@ export class SearchComponent implements OnInit {
|
||||
);
|
||||
const searchOptions$: Observable<PaginatedSearchOptions> = this.getSearchOptions().pipe(distinctUntilChanged());
|
||||
|
||||
this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe(
|
||||
filter(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => {
|
||||
this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$, this.currentScope$]).pipe(
|
||||
filter(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => {
|
||||
// filter for search options related to instanced paginated id
|
||||
return searchOptions.pagination.id === this.paginationId;
|
||||
}),
|
||||
debounceTime(100)
|
||||
).subscribe(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => {
|
||||
).subscribe(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => {
|
||||
// Build the PaginatedSearchOptions object
|
||||
const combinedOptions = Object.assign({}, searchOptions,
|
||||
{
|
||||
@@ -337,6 +345,9 @@ export class SearchComponent implements OnInit {
|
||||
if (combinedOptions.query === '') {
|
||||
combinedOptions.query = this.query;
|
||||
}
|
||||
if (isEmpty(combinedOptions.scope)) {
|
||||
combinedOptions.scope = scope;
|
||||
}
|
||||
const newSearchOptions = new PaginatedSearchOptions(combinedOptions);
|
||||
// check if search options are changed
|
||||
// if so retrieve new related results otherwise skip it
|
||||
@@ -344,13 +355,12 @@ export class SearchComponent implements OnInit {
|
||||
// Initialize variables
|
||||
this.currentConfiguration$.next(configuration);
|
||||
this.currentSortOptions$.next(newSearchOptions.sort);
|
||||
this.currentScope$.next(newSearchOptions.scope);
|
||||
this.sortOptionsList$.next(searchSortOptions);
|
||||
this.searchOptions$.next(newSearchOptions);
|
||||
this.initialized$.next(true);
|
||||
// retrieve results
|
||||
this.retrieveSearchResults(newSearchOptions);
|
||||
this.retrieveFilters(searchOptions);
|
||||
this.retrieveFilters(newSearchOptions);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@@ -15,12 +15,38 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-search',
|
||||
styleUrls: [],
|
||||
templateUrl: '../theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
|
||||
|
||||
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query'];
|
||||
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = [
|
||||
'configurationList',
|
||||
'context',
|
||||
'configuration',
|
||||
'fixedFilterQuery',
|
||||
'useCachedVersionIfAvailable',
|
||||
'inPlaceSearch',
|
||||
'linkType',
|
||||
'paginationId',
|
||||
'searchEnabled',
|
||||
'sideBarWidth',
|
||||
'searchFormPlaceholder',
|
||||
'selectable',
|
||||
'selectionConfig',
|
||||
'showCsvExport',
|
||||
'showSidebar',
|
||||
'showThumbnails',
|
||||
'showViewModes',
|
||||
'useUniquePageId',
|
||||
'viewModeList',
|
||||
'showScopeSelector',
|
||||
'trackStatistics',
|
||||
'query',
|
||||
'scope',
|
||||
'resultFound',
|
||||
'deselectObject',
|
||||
'selectObject',
|
||||
];
|
||||
|
||||
@Input() configurationList: SearchConfigurationOption[];
|
||||
|
||||
@@ -52,7 +78,7 @@ export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
|
||||
|
||||
@Input() showSidebar: boolean;
|
||||
|
||||
@Input() showThumbnails;
|
||||
@Input() showThumbnails: boolean;
|
||||
|
||||
@Input() showViewModes: boolean;
|
||||
|
||||
@@ -66,6 +92,8 @@ export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
|
||||
|
||||
@Input() query: string;
|
||||
|
||||
@Input() scope: string;
|
||||
|
||||
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
|
||||
|
||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
|
||||
|
@@ -80,6 +80,9 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
||||
const fileData: any = mockUploadFiles[0];
|
||||
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
|
||||
|
||||
let noAccessConditionsMock = Object.assign({}, mockFileFormData);
|
||||
delete noAccessConditionsMock.accessConditions;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -299,6 +302,28 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
||||
|
||||
}));
|
||||
|
||||
it('should update Bitstream data properly when access options are omitted', fakeAsync(() => {
|
||||
compAsAny.formRef = {formGroup: null};
|
||||
compAsAny.fileData = fileData;
|
||||
compAsAny.pathCombiner = pathCombiner;
|
||||
formService.validateAllFormFields.and.callFake(() => null);
|
||||
formService.isValid.and.returnValue(of(true));
|
||||
formService.getFormData.and.returnValue(of(noAccessConditionsMock));
|
||||
const response = [
|
||||
Object.assign(mockSubmissionObject, {
|
||||
sections: {
|
||||
upload: {
|
||||
files: mockUploadFiles
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
|
||||
comp.saveBitstreamData();
|
||||
tick();
|
||||
expect(uploadService.updateFileData).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
|
||||
compAsAny.formRef = {formGroup: null};
|
||||
compAsAny.pathCombiner = pathCombiner;
|
||||
|
@@ -416,7 +416,9 @@ export class SubmissionSectionUploadFileEditComponent
|
||||
this.operationsBuilder.remove(this.pathCombiner.getPath(path));
|
||||
});
|
||||
const accessConditionsToSave = [];
|
||||
if (formData.hasOwnProperty('accessConditions')) {
|
||||
formData.accessConditions
|
||||
.filter((accessConditions) => isNotNull(accessConditions))
|
||||
.map((accessConditions) => accessConditions.accessConditionGroup)
|
||||
.filter((accessCondition) => isNotEmpty(accessCondition))
|
||||
.forEach((accessCondition) => {
|
||||
@@ -465,7 +467,7 @@ export class SubmissionSectionUploadFileEditComponent
|
||||
accessConditionsToSave.push(currentAccessCondition);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
if (isNotEmpty(accessConditionsToSave)) {
|
||||
this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true);
|
||||
}
|
||||
|
97
src/app/suggestion-notifications/selectors.ts
Normal file
97
src/app/suggestion-notifications/selectors.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/store';
|
||||
import { suggestionNotificationsSelector, SuggestionNotificationsState } from '../notifications/notifications.reducer';
|
||||
import { SuggestionTarget } from '../core/suggestion-notifications/models/suggestion-target.model';
|
||||
import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer';
|
||||
import {subStateSelector} from '../submission/selectors';
|
||||
|
||||
/**
|
||||
* Returns the Reciter Suggestion Target state.
|
||||
* @function _getSuggestionTargetState
|
||||
* @param {AppState} state Top level state.
|
||||
* @return {SuggestionNotificationsState}
|
||||
*/
|
||||
const _getSuggestionTargetState = createFeatureSelector<SuggestionNotificationsState>('suggestionNotifications');
|
||||
|
||||
// Reciter Suggestion Targets
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the Suggestion Targets State.
|
||||
* @function suggestionTargetStateSelector
|
||||
* @return {SuggestionNotificationsState}
|
||||
*/
|
||||
export function suggestionTargetStateSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTargetState> {
|
||||
return subStateSelector<SuggestionNotificationsState, SuggestionTargetState>(suggestionNotificationsSelector, 'suggestionTarget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Suggestion Targets list.
|
||||
* @function suggestionTargetObjectSelector
|
||||
* @return {SuggestionTarget[]}
|
||||
*/
|
||||
export function suggestionTargetObjectSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTarget[]> {
|
||||
return subStateSelector<SuggestionNotificationsState, SuggestionTarget[]>(suggestionTargetStateSelector(), 'targets');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Suggestion Targets are loaded.
|
||||
* @function isSuggestionTargetLoadedSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isSuggestionTargetLoadedSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.loaded
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the deduplication sets are processing.
|
||||
* @function isDeduplicationSetsProcessingSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isReciterSuggestionTargetProcessingSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.processing
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the total available pages of Reciter Suggestion Targets.
|
||||
* @function getSuggestionTargetTotalPagesSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getSuggestionTargetTotalPagesSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.totalPages
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the current page of Suggestion Targets.
|
||||
* @function getSuggestionTargetCurrentPageSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getSuggestionTargetCurrentPageSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentPage
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the total number of Suggestion Targets.
|
||||
* @function getSuggestionTargetTotalsSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getSuggestionTargetTotalsSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.totalElements
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns Suggestion Targets for the current user.
|
||||
* @function getCurrentUserSuggestionTargetSelector
|
||||
* @return {SuggestionTarget[]}
|
||||
*/
|
||||
export const getCurrentUserSuggestionTargetsSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns whether or not the user has consulted their suggestions
|
||||
* @function getCurrentUserSuggestionTargetSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited
|
||||
);
|
@@ -0,0 +1,29 @@
|
||||
<div class="d-inline">
|
||||
<div ngbDropdown class="d-inline">
|
||||
<button *ngIf="isCollectionFixed; else chooseCollection" class="btn btn-success" type="button" (click)="approveAndImportCollectionFixed()">
|
||||
<i class="fa fa-check" aria-hidden="true"></i> {{ approveAndImportLabel() | translate}}
|
||||
</button>
|
||||
<ng-template #chooseCollection>
|
||||
<button class="btn btn-success" id="dropdownSubmission" ngbDropdownToggle
|
||||
type="button">
|
||||
<i class="fa fa-check" aria-hidden="true"></i> {{ approveAndImportLabel() | translate}}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu
|
||||
class="dropdown-menu"
|
||||
id="entityControlsDropdownMenu"
|
||||
aria-labelledby="dropdownSubmission">
|
||||
<ds-entity-dropdown (selectionChange)="openDialog($event)"></ds-entity-dropdown>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
<button (click)="ignoreSuggestion()" class="btn btn-danger ml-2"><i class="fa fa-ban"></i>
|
||||
{{ ignoreSuggestionLabel() | translate}}</button>
|
||||
<button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [disabled]="!hasEvidence" class="btn btn-info ml-2">
|
||||
<i class="fa fa-eye"></i>
|
||||
<ng-container *ngIf="!seeEvidence"> {{ 'suggestion.seeEvidence' | translate}}</ng-container>
|
||||
<ng-container *ngIf="seeEvidence"> {{ 'suggestion.hideEvidence' | translate}}</ng-container>
|
||||
</button>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,95 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model';
|
||||
import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-list-element.component';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
|
||||
/**
|
||||
* Show and trigger the actions to submit for a suggestion
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-suggestion-actions',
|
||||
styleUrls: [ './suggestion-actions.component.scss' ],
|
||||
templateUrl: './suggestion-actions.component.html'
|
||||
})
|
||||
export class SuggestionActionsComponent {
|
||||
|
||||
@Input() object: Suggestion;
|
||||
|
||||
@Input() isBulk = false;
|
||||
|
||||
@Input() hasEvidence = false;
|
||||
|
||||
@Input() seeEvidence = false;
|
||||
|
||||
@Input() isCollectionFixed = false;
|
||||
|
||||
/**
|
||||
* The component is used to Delete suggestion
|
||||
*/
|
||||
@Output() ignoreSuggestionClicked = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* The component is used to approve & import
|
||||
*/
|
||||
@Output() approveAndImport = new EventEmitter<SuggestionApproveAndImport>();
|
||||
|
||||
/**
|
||||
* The component is used to approve & import
|
||||
*/
|
||||
@Output() seeEvidences = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private modalService: NgbModal) { }
|
||||
|
||||
/**
|
||||
* Method called on clicking the button "approve & import", It opens a dialog for
|
||||
* select a collection and it emits an approveAndImport event.
|
||||
*/
|
||||
openDialog(entity: ItemType) {
|
||||
|
||||
const modalRef = this.modalService.open(CreateItemParentSelectorComponent);
|
||||
modalRef.componentInstance.emitOnly = true;
|
||||
modalRef.componentInstance.entityType = entity.label;
|
||||
|
||||
modalRef.componentInstance.select.pipe(take(1))
|
||||
.subscribe((collection: Collection) => {
|
||||
this.approveAndImport.emit({
|
||||
suggestion: this.isBulk ? undefined : this.object,
|
||||
collectionId: collection.id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
approveAndImportCollectionFixed() {
|
||||
this.approveAndImport.emit({
|
||||
suggestion: this.isBulk ? undefined : this.object,
|
||||
collectionId: null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete the suggestion
|
||||
*/
|
||||
ignoreSuggestion() {
|
||||
this.ignoreSuggestionClicked.emit(this.isBulk ? undefined : this.object.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle See Evidence
|
||||
*/
|
||||
toggleSeeEvidences() {
|
||||
this.seeEvidences.emit(!this.seeEvidence);
|
||||
}
|
||||
|
||||
ignoreSuggestionLabel(): string {
|
||||
return this.isBulk ? 'suggestion.ignoreSuggestion.bulk' : 'suggestion.ignoreSuggestion' ;
|
||||
}
|
||||
|
||||
approveAndImportLabel(): string {
|
||||
return this.isBulk ? 'suggestion.approveAndImport.bulk' : 'suggestion.approveAndImport';
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<div class="table-responsive" *ngIf="evidences">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{'suggestion.evidence.score' | translate}}</th>
|
||||
<th>{{'suggestion.evidence.type' | translate}}</th>
|
||||
<th>{{'suggestion.evidence.notes' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let evidence of evidences | dsObjectKeys">
|
||||
<td>{{evidences[evidence].score}}</td>
|
||||
<td>{{evidence | translate}}</td>
|
||||
<td>{{evidences[evidence].notes}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { fadeIn } from '../../../shared/animations/fade';
|
||||
import { SuggestionEvidences } from '../../../core/suggestion-notifications/models/suggestion.model';
|
||||
|
||||
/**
|
||||
* Show suggestion evidences such as score (authorScore, dateScore)
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-suggestion-evidences',
|
||||
styleUrls: [ './suggestion-evidences.component.scss' ],
|
||||
templateUrl: './suggestion-evidences.component.html',
|
||||
animations: [fadeIn]
|
||||
})
|
||||
export class SuggestionEvidencesComponent {
|
||||
|
||||
@Input() evidences: SuggestionEvidences;
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- Bulk Selection Panel -->
|
||||
<div class="col-1 text-center align-self-center">
|
||||
<div class="">
|
||||
<input type="checkbox" class="form-check-input"
|
||||
[checked]="isSelected" (change)="changeSelected($event)"
|
||||
[attr.aria-label]="object.display"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Total Score Panel -->
|
||||
<div class="col-2 text-center align-self-center">
|
||||
<div class="">
|
||||
<div><strong> {{'suggestion.totalScore' | translate}}</strong> </div>
|
||||
<span class="suggestion-score">{{ object.score }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Suggestion Panel -->
|
||||
<div class="col">
|
||||
<!-- Object Preview -->
|
||||
<ds-item-search-result-list-element
|
||||
[showLabel]="false"
|
||||
[showThumbnails]="false"
|
||||
[object]="listableObject"
|
||||
[linkType]="0"
|
||||
></ds-item-search-result-list-element>
|
||||
<!-- Actions -->
|
||||
<ds-suggestion-actions class="parent mt-2" [hasEvidence]="hasEvidences()"
|
||||
[seeEvidence]="seeEvidence"
|
||||
[object]="object"
|
||||
[isCollectionFixed]="isCollectionFixed"
|
||||
(approveAndImport)="onApproveAndImport($event)"
|
||||
(seeEvidences)="onSeeEvidences($event)"
|
||||
(ignoreSuggestionClicked)="onIgnoreSuggestion($event)"
|
||||
></ds-suggestion-actions>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Evidences Panel -->
|
||||
<div *ngIf="seeEvidence" class="mt-2 row">
|
||||
<div class="col offset-3">
|
||||
<ds-suggestion-evidences [evidences]="object.evidences"></ds-suggestion-evidences>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
@@ -0,0 +1,16 @@
|
||||
.issue-date {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.parent {
|
||||
display: flex;
|
||||
gap:10px;
|
||||
}
|
||||
|
||||
.import {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
.suggestion-score {
|
||||
font-size: 1.5rem;
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
import { SuggestionListElementComponent } from './suggestion-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { mockSuggestionPublicationOne } from '../../shared/mocks/publication-claim.mock';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
|
||||
describe('SuggestionListElementComponent', () => {
|
||||
let component: SuggestionListElementComponent;
|
||||
let fixture: ComponentFixture<SuggestionListElementComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [SuggestionListElementComponent],
|
||||
providers: [
|
||||
NgbModal
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents().then();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SuggestionListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
component.object = mockSuggestionPublicationOne;
|
||||
});
|
||||
|
||||
describe('SuggestionListElementComponent test', () => {
|
||||
|
||||
it('should create', () => {
|
||||
scheduler.schedule(() => fixture.detectChanges());
|
||||
scheduler.flush();
|
||||
const expectedIndexableObject = Object.assign(new Item(), {
|
||||
id: mockSuggestionPublicationOne.id,
|
||||
metadata: mockSuggestionPublicationOne.metadata
|
||||
});
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.listableObject.hitHighlights).toEqual({});
|
||||
expect(component.listableObject.indexableObject).toEqual(expectedIndexableObject);
|
||||
});
|
||||
|
||||
it('should check if has evidence', () => {
|
||||
expect(component.hasEvidences()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set seeEvidences', () => {
|
||||
component.onSeeEvidences(true);
|
||||
expect(component.seeEvidence).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should emit selection', () => {
|
||||
spyOn(component.selected, 'next');
|
||||
component.changeSelected({target: { checked: true}});
|
||||
expect(component.selected.next).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should emit for deletion', () => {
|
||||
spyOn(component.ignoreSuggestionClicked, 'emit');
|
||||
component.onIgnoreSuggestion('1234');
|
||||
expect(component.ignoreSuggestionClicked.emit).toHaveBeenCalledWith('1234');
|
||||
});
|
||||
|
||||
it('should emit for approve and import', () => {
|
||||
const event = {collectionId:'1234', suggestion: mockSuggestionPublicationOne};
|
||||
spyOn(component.approveAndImport, 'emit');
|
||||
component.onApproveAndImport(event);
|
||||
expect(component.approveAndImport.emit).toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,104 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { fadeIn } from '../../shared/animations/fade';
|
||||
import { Suggestion } from '../../core/suggestion-notifications/models/suggestion.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* A simple interface to unite a specific suggestion and the id of the chosen collection
|
||||
*/
|
||||
export interface SuggestionApproveAndImport {
|
||||
suggestion: Suggestion;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all the suggestions by researcher
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-suggestion-list-item',
|
||||
styleUrls: ['./suggestion-list-element.component.scss'],
|
||||
templateUrl: './suggestion-list-element.component.html',
|
||||
animations: [fadeIn]
|
||||
})
|
||||
export class SuggestionListElementComponent implements OnInit {
|
||||
|
||||
@Input() object: Suggestion;
|
||||
|
||||
@Input() isSelected = false;
|
||||
|
||||
@Input() isCollectionFixed = false;
|
||||
|
||||
public listableObject: any;
|
||||
|
||||
public seeEvidence = false;
|
||||
|
||||
/**
|
||||
* The component is used to Delete suggestion
|
||||
*/
|
||||
@Output() ignoreSuggestionClicked = new EventEmitter();
|
||||
|
||||
/**
|
||||
* The component is used to approve & import
|
||||
*/
|
||||
@Output() approveAndImport = new EventEmitter();
|
||||
|
||||
/**
|
||||
* New value whether the element is selected
|
||||
*/
|
||||
@Output() selected = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {NgbModal} modalService
|
||||
*/
|
||||
constructor(private modalService: NgbModal) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.listableObject = {
|
||||
indexableObject: Object.assign(new Item(), {id: this.object.id, metadata: this.object.metadata}),
|
||||
hitHighlights: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve and import the suggestion
|
||||
*/
|
||||
onApproveAndImport(event: SuggestionApproveAndImport) {
|
||||
this.approveAndImport.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the suggestion
|
||||
*/
|
||||
onIgnoreSuggestion(suggestionId: string) {
|
||||
this.ignoreSuggestionClicked.emit(suggestionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change is selected value.
|
||||
*/
|
||||
changeSelected(event) {
|
||||
this.isSelected = event.target.checked;
|
||||
this.selected.next(this.isSelected);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the Evidence
|
||||
*/
|
||||
hasEvidences() {
|
||||
return isNotEmpty(this.object.evidences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the see evidence variable.
|
||||
*/
|
||||
onSeeEvidences(seeEvidence: boolean) {
|
||||
this.seeEvidence = seeEvidence;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 id="header" class="border-bottom pb-2">{{'suggestion.title'| translate}}</h1>
|
||||
|
||||
<ds-loading class="container" *ngIf="(isTargetsLoading() | async)" message="{{'suggestion.loading' | translate}}"></ds-loading>
|
||||
<ds-pagination *ngIf="!(isTargetsLoading() | async)"
|
||||
[paginationOptions]="paginationConfig"
|
||||
[collectionSize]="(totalElements$ | async)"
|
||||
[hideGear]="false"
|
||||
[retainScrollPosition]="false"
|
||||
(paginationChange)="getSuggestionTargets()">
|
||||
|
||||
<ds-loading class="container" *ngIf="(isTargetsProcessing() | async)" message="'suggestion.loading' | translate"></ds-loading>
|
||||
<ng-container *ngIf="!(isTargetsProcessing() | async)">
|
||||
<div *ngIf="!(targets$|async) || (targets$|async)?.length == 0" class="alert alert-info w-100 mb-2 mt-2" role="alert">
|
||||
{{'suggestion.noTargets' | translate}}
|
||||
</div>
|
||||
<div *ngIf="(targets$|async) && (targets$|async)?.length != 0" class="table-responsive mt-2">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr class="text-center">
|
||||
<th scope="col">{{'suggestion.table.name' | translate}}</th>
|
||||
<th scope="col">{{'suggestion.table.actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let targetElement of (targets$ | async); let i = index" class="text-center">
|
||||
<td>
|
||||
<a target="_blank" [routerLink]="['/entities/person/', getTargetUuid(targetElement)]">{{targetElement.display}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="redirectToSuggestions(targetElement.id)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{('suggestion.button.review.title' | translate: { total: targetElement.total.toString() }) +
|
||||
targetElement.display}}">
|
||||
<span>{{'suggestion.button.review' | translate: { total: targetElement.total.toString() } }} </span>
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,143 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged, take } from 'rxjs/operators';
|
||||
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-targets.state.service';
|
||||
import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths';
|
||||
import { SuggestionsService } from '../../suggestions.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Component to display the Suggestion Target list.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-publication-claim',
|
||||
templateUrl: './publication-claim.component.html',
|
||||
styleUrls: ['./publication-claim.component.scss'],
|
||||
})
|
||||
export class PublicationClaimComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The source for which to list targets
|
||||
*/
|
||||
@Input() source: string;
|
||||
|
||||
/**
|
||||
* The pagination system configuration for HTML listing.
|
||||
* @type {PaginationComponentOptions}
|
||||
*/
|
||||
public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'stp',
|
||||
pageSizeOptions: [5, 10, 20, 40, 60]
|
||||
});
|
||||
|
||||
/**
|
||||
* The Suggestion Target list.
|
||||
*/
|
||||
public targets$: Observable<SuggestionTarget[]>;
|
||||
/**
|
||||
* The total number of Suggestion Targets.
|
||||
*/
|
||||
public totalElements$: Observable<number>;
|
||||
/**
|
||||
* Array to track all the component subscriptions. Useful to unsubscribe them with 'onDestroy'.
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Initialize the component variables.
|
||||
* @param {PaginationService} paginationService
|
||||
* @param {SuggestionTargetsStateService} suggestionTargetsStateService
|
||||
* @param {SuggestionsService} suggestionService
|
||||
* @param {Router} router
|
||||
*/
|
||||
constructor(
|
||||
private paginationService: PaginationService,
|
||||
private suggestionTargetsStateService: SuggestionTargetsStateService,
|
||||
private suggestionService: SuggestionsService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component initialization.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets();
|
||||
this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals();
|
||||
|
||||
this.subs.push(
|
||||
this.suggestionTargetsStateService.isSuggestionTargetsLoaded().pipe(
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.getSuggestionTargets();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loading, 'false' otherwise.
|
||||
*/
|
||||
public isTargetsLoading(): Observable<boolean> {
|
||||
return this.suggestionTargetsStateService.isSuggestionTargetsLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the processing status of the Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
|
||||
*/
|
||||
public isTargetsProcessing(): Observable<boolean> {
|
||||
return this.suggestionTargetsStateService.isSuggestionTargetsProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to suggestion page.
|
||||
*
|
||||
* @param {string} id
|
||||
* the id of suggestion target
|
||||
*/
|
||||
public redirectToSuggestions(id: string) {
|
||||
this.router.navigate([getSuggestionPageRoute(id)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction();
|
||||
this.subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the Suggestion Targets retrival.
|
||||
*/
|
||||
public getSuggestionTargets(): void {
|
||||
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
|
||||
distinctUntilChanged(),
|
||||
take(1)
|
||||
).subscribe((options: PaginationComponentOptions) => {
|
||||
this.suggestionTargetsStateService.dispatchRetrieveSuggestionTargets(
|
||||
this.source,
|
||||
options.pageSize,
|
||||
options.currentPage
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getTargetUuid(target: SuggestionTarget) {
|
||||
return this.suggestionService.getTargetUuid(target);
|
||||
}
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const SuggestionTargetActionTypes = {
|
||||
ADD_TARGETS: type('dspace/integration/openaire/suggestions/target/ADD_TARGETS'),
|
||||
CLEAR_TARGETS: type('dspace/integration/openaire/suggestions/target/CLEAR_TARGETS'),
|
||||
RETRIEVE_TARGETS_BY_SOURCE: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE'),
|
||||
RETRIEVE_TARGETS_BY_SOURCE_ERROR: type('dspace/integration/openaire/suggestions/target/RETRIEVE_TARGETS_BY_SOURCE_ERROR'),
|
||||
ADD_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/ADD_USER_SUGGESTIONS'),
|
||||
REFRESH_USER_SUGGESTIONS: type('dspace/integration/openaire/suggestions/target/REFRESH_USER_SUGGESTIONS'),
|
||||
MARK_USER_SUGGESTIONS_AS_VISITED: type('dspace/integration/openaire/suggestions/target/MARK_USER_SUGGESTIONS_AS_VISITED')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* An ngrx action to retrieve all the Suggestion Targets.
|
||||
*/
|
||||
export class RetrieveTargetsBySourceAction implements Action {
|
||||
type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE;
|
||||
payload: {
|
||||
source: string;
|
||||
elementsPerPage: number;
|
||||
currentPage: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new RetrieveTargetsBySourceAction.
|
||||
*
|
||||
* @param source
|
||||
* the source for which to retrieve suggestion targets
|
||||
* @param elementsPerPage
|
||||
* the number of targets per page
|
||||
* @param currentPage
|
||||
* The page number to retrieve
|
||||
*/
|
||||
constructor(source: string, elementsPerPage: number, currentPage: number) {
|
||||
this.payload = {
|
||||
source,
|
||||
elementsPerPage,
|
||||
currentPage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action for retrieving 'all Suggestion Targets' error.
|
||||
*/
|
||||
export class RetrieveAllTargetsErrorAction implements Action {
|
||||
type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to load the Suggestion Target objects.
|
||||
*/
|
||||
export class AddTargetAction implements Action {
|
||||
type = SuggestionTargetActionTypes.ADD_TARGETS;
|
||||
payload: {
|
||||
targets: SuggestionTarget[];
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
totalElements: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddTargetAction.
|
||||
*
|
||||
* @param targets
|
||||
* the list of targets
|
||||
* @param totalPages
|
||||
* the total available pages of targets
|
||||
* @param currentPage
|
||||
* the current page
|
||||
* @param totalElements
|
||||
* the total available Suggestion Targets
|
||||
*/
|
||||
constructor(targets: SuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) {
|
||||
this.payload = {
|
||||
targets,
|
||||
totalPages,
|
||||
currentPage,
|
||||
totalElements
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to load the user Suggestion Target object.
|
||||
* Called by the ??? effect.
|
||||
*/
|
||||
export class AddUserSuggestionsAction implements Action {
|
||||
type = SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS;
|
||||
payload: {
|
||||
suggestionTargets: SuggestionTarget[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddUserSuggestionsAction.
|
||||
*
|
||||
* @param suggestionTargets
|
||||
* the user suggestions target
|
||||
*/
|
||||
constructor(suggestionTargets: SuggestionTarget[]) {
|
||||
this.payload = { suggestionTargets };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to reload the user Suggestion Target object.
|
||||
* Called by the ??? effect.
|
||||
*/
|
||||
export class RefreshUserSuggestionsAction implements Action {
|
||||
type = SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to Mark User Suggestions As Visited.
|
||||
* Called by the ??? effect.
|
||||
*/
|
||||
export class MarkUserSuggestionsAsVisitedAction implements Action {
|
||||
type = SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED;
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to clear targets state.
|
||||
*/
|
||||
export class ClearSuggestionTargetsAction implements Action {
|
||||
type = SuggestionTargetActionTypes.CLEAR_TARGETS;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types.
|
||||
*/
|
||||
export type SuggestionTargetsActions
|
||||
= AddTargetAction
|
||||
| AddUserSuggestionsAction
|
||||
| ClearSuggestionTargetsAction
|
||||
| MarkUserSuggestionsAsVisitedAction
|
||||
| RetrieveTargetsBySourceAction
|
||||
| RetrieveAllTargetsErrorAction
|
||||
| RefreshUserSuggestionsAction;
|
@@ -0,0 +1,97 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import {
|
||||
AddTargetAction,
|
||||
AddUserSuggestionsAction,
|
||||
RetrieveAllTargetsErrorAction,
|
||||
RetrieveTargetsBySourceAction,
|
||||
SuggestionTargetActionTypes,
|
||||
} from './suggestion-targets.actions';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
|
||||
/**
|
||||
* Provides effect methods for the Suggestion Targets actions.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SuggestionTargetsEffects {
|
||||
|
||||
/**
|
||||
* Retrieve all Suggestion Targets managing pagination and errors.
|
||||
*/
|
||||
retrieveTargetsBySource$ = createEffect(() => this.actions$.pipe(
|
||||
ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE),
|
||||
switchMap((action: RetrieveTargetsBySourceAction) => {
|
||||
return this.suggestionsService.getTargets(
|
||||
action.payload.source,
|
||||
action.payload.elementsPerPage,
|
||||
action.payload.currentPage
|
||||
).pipe(
|
||||
map((targets: PaginatedList<SuggestionTarget>) =>
|
||||
new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements)
|
||||
),
|
||||
catchError((error: Error) => {
|
||||
if (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
return of(new RetrieveAllTargetsErrorAction());
|
||||
})
|
||||
);
|
||||
})
|
||||
));
|
||||
|
||||
/**
|
||||
* Show a notification on error.
|
||||
*/
|
||||
retrieveAllTargetsErrorAction$ = createEffect(() => this.actions$.pipe(
|
||||
ofType(SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR),
|
||||
tap(() => {
|
||||
this.notificationsService.error(null, this.translate.get('suggestion.target.error.service.retrieve'));
|
||||
})
|
||||
), { dispatch: false });
|
||||
|
||||
/**
|
||||
* Fetch the current user suggestion
|
||||
*/
|
||||
refreshUserSuggestionsAction$ = createEffect(() => this.actions$.pipe(
|
||||
ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS),
|
||||
switchMap(() => {
|
||||
return this.store$.select((state: any) => state.core.auth.userId)
|
||||
.pipe(
|
||||
switchMap((userId: string) => {
|
||||
return this.suggestionsService.retrieveCurrentUserSuggestions(userId)
|
||||
.pipe(
|
||||
map((suggestionTargets: SuggestionTarget[]) => new AddUserSuggestionsAction(suggestionTargets)),
|
||||
catchError((errors) => of(errors))
|
||||
);
|
||||
}),
|
||||
catchError((errors) => of(errors))
|
||||
);
|
||||
}))
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the effect class variables.
|
||||
* @param {Actions} actions$
|
||||
* @param {Store<any>} store$
|
||||
* @param {TranslateService} translate
|
||||
* @param {NotificationsService} notificationsService
|
||||
* @param {SuggestionsService} suggestionsService
|
||||
*/
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store$: Store<any>,
|
||||
private translate: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private suggestionsService: SuggestionsService
|
||||
) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
|
||||
/**
|
||||
* The interface representing the OpenAIRE suggestion targets state.
|
||||
*/
|
||||
export interface SuggestionTargetState {
|
||||
targets: SuggestionTarget[];
|
||||
processing: boolean;
|
||||
loaded: boolean;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
totalElements: number;
|
||||
currentUserTargets: SuggestionTarget[];
|
||||
currentUserTargetsVisited: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the OpenAIRE Suggestion Target state initialization.
|
||||
*/
|
||||
const SuggestionTargetInitialState: SuggestionTargetState = {
|
||||
targets: [],
|
||||
processing: false,
|
||||
loaded: false,
|
||||
totalPages: 0,
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
currentUserTargets: null,
|
||||
currentUserTargetsVisited: false
|
||||
};
|
||||
|
||||
/**
|
||||
* The OpenAIRE Broker Topic Reducer
|
||||
*
|
||||
* @param state
|
||||
* the current state initialized with SuggestionTargetInitialState
|
||||
* @param action
|
||||
* the action to perform on the state
|
||||
* @return SuggestionTargetState
|
||||
* the new state
|
||||
*/
|
||||
export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState {
|
||||
switch (action.type) {
|
||||
case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: {
|
||||
return Object.assign({}, state, {
|
||||
targets: [],
|
||||
processing: true
|
||||
});
|
||||
}
|
||||
|
||||
case SuggestionTargetActionTypes.ADD_TARGETS: {
|
||||
return Object.assign({}, state, {
|
||||
targets: state.targets.concat(action.payload.targets),
|
||||
processing: false,
|
||||
loaded: true,
|
||||
totalPages: action.payload.totalPages,
|
||||
currentPage: state.currentPage,
|
||||
totalElements: action.payload.totalElements
|
||||
});
|
||||
}
|
||||
|
||||
case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: {
|
||||
return Object.assign({}, state, {
|
||||
targets: [],
|
||||
processing: false,
|
||||
loaded: true,
|
||||
totalPages: 0,
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
});
|
||||
}
|
||||
|
||||
case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: {
|
||||
return Object.assign({}, state, {
|
||||
currentUserTargets: action.payload.suggestionTargets
|
||||
});
|
||||
}
|
||||
|
||||
case SuggestionTargetActionTypes.MARK_USER_SUGGESTIONS_AS_VISITED: {
|
||||
return Object.assign({}, state, {
|
||||
currentUserTargetsVisited: true
|
||||
});
|
||||
}
|
||||
|
||||
case SuggestionTargetActionTypes.CLEAR_TARGETS: {
|
||||
return Object.assign({}, state, {
|
||||
targets: [],
|
||||
processing: false,
|
||||
loaded: false,
|
||||
totalPages: 0,
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,164 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
getCurrentUserSuggestionTargetsSelector,
|
||||
getCurrentUserSuggestionTargetsVisitedSelector,
|
||||
getSuggestionTargetCurrentPageSelector,
|
||||
getSuggestionTargetTotalsSelector,
|
||||
isSuggestionTargetLoadedSelector,
|
||||
isReciterSuggestionTargetProcessingSelector,
|
||||
suggestionTargetObjectSelector
|
||||
} from '../selectors';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/models/suggestion-target.model';
|
||||
import {
|
||||
ClearSuggestionTargetsAction,
|
||||
MarkUserSuggestionsAsVisitedAction,
|
||||
RefreshUserSuggestionsAction,
|
||||
RetrieveTargetsBySourceAction
|
||||
} from './suggestion-targets.actions';
|
||||
import { SuggestionNotificationsState } from '../../notifications/notifications.reducer';
|
||||
|
||||
/**
|
||||
* The service handling the Suggestion targets State.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SuggestionTargetsStateService {
|
||||
|
||||
/**
|
||||
* Initialize the service variables.
|
||||
* @param {Store<SuggestionNotificationsState>} store
|
||||
*/
|
||||
constructor(private store: Store<SuggestionNotificationsState>) { }
|
||||
|
||||
/**
|
||||
* Returns the list of Suggestion Targets from the state.
|
||||
*
|
||||
* @return Observable<SuggestionTarget>
|
||||
* The list of Suggestion Targets.
|
||||
*/
|
||||
public getSuggestionTargets(): Observable<SuggestionTarget[]> {
|
||||
return this.store.pipe(select(suggestionTargetObjectSelector()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loading, 'false' otherwise.
|
||||
*/
|
||||
public isSuggestionTargetsLoading(): Observable<boolean> {
|
||||
return this.store.pipe(
|
||||
select(isSuggestionTargetLoadedSelector),
|
||||
map((loaded: boolean) => !loaded)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Suggestion Targets (whether or not they were loaded).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loaded, 'false' otherwise.
|
||||
*/
|
||||
public isSuggestionTargetsLoaded(): Observable<boolean> {
|
||||
return this.store.pipe(select(isSuggestionTargetLoadedSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the processing status of the Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
|
||||
*/
|
||||
public isSuggestionTargetsProcessing(): Observable<boolean> {
|
||||
return this.store.pipe(select(isReciterSuggestionTargetProcessingSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, from the state, the total available pages of the Suggestion Targets.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the Suggestion Targets pages.
|
||||
*/
|
||||
public getSuggestionTargetsTotalPages(): Observable<number> {
|
||||
return this.store.pipe(select(getSuggestionTargetTotalsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current page of the Suggestion Targets, from the state.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the current Suggestion Targets page.
|
||||
*/
|
||||
public getSuggestionTargetsCurrentPage(): Observable<number> {
|
||||
return this.store.pipe(select(getSuggestionTargetCurrentPageSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of the Suggestion Targets.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the Suggestion Targets.
|
||||
*/
|
||||
public getSuggestionTargetsTotals(): Observable<number> {
|
||||
return this.store.pipe(select(getSuggestionTargetTotalsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a request to change the Suggestion Targets state, retrieving the targets from the server.
|
||||
*
|
||||
* @param source
|
||||
* the source for which to retrieve suggestion targets
|
||||
* @param elementsPerPage
|
||||
* The number of the targets per page.
|
||||
* @param currentPage
|
||||
* The number of the current page.
|
||||
*/
|
||||
public dispatchRetrieveSuggestionTargets(source: string, elementsPerPage: number, currentPage: number): void {
|
||||
this.store.dispatch(new RetrieveTargetsBySourceAction(source, elementsPerPage, currentPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, from the state, the suggestion targets for the current user.
|
||||
*
|
||||
* @return Observable<OpenaireReciterSuggestionTarget>
|
||||
* The Suggestion Targets object.
|
||||
*/
|
||||
public getCurrentUserSuggestionTargets(): Observable<SuggestionTarget[]> {
|
||||
return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, from the state, whether or not the user has consulted their suggestion targets.
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* True if user already visited, false otherwise.
|
||||
*/
|
||||
public hasUserVisitedSuggestions(): Observable<boolean> {
|
||||
return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a new MarkUserSuggestionsAsVisitedAction
|
||||
*/
|
||||
public dispatchMarkUserSuggestionsAsVisitedAction(): void {
|
||||
this.store.dispatch(new MarkUserSuggestionsAsVisitedAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an action to clear the Reciter Suggestion Targets state.
|
||||
*/
|
||||
public dispatchClearSuggestionTargetsAction(): void {
|
||||
this.store.dispatch(new ClearSuggestionTargetsAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an action to refresh the user suggestions.
|
||||
*/
|
||||
public dispatchRefreshUserSuggestionsAction(): void {
|
||||
this.store.dispatch(new RefreshUserSuggestionsAction());
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user