mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-11 20:13:07 +00:00
Merge branch 'CST-5249_suggestion' of https://github.com/4Science/dspace-angular into CST-11299
This commit is contained in:
@@ -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 NOTIFICATIONS_RECITER_SUGGESTION_PATH = 'suggestion-targets';
|
||||
|
||||
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 { NOTIFICATIONS_RECITER_SUGGESTION_PATH } from './admin-notifications-routing-paths';
|
||||
import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component';
|
||||
import { AdminNotificationsSuggestionTargetsPageResolver } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service';
|
||||
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';
|
||||
@@ -16,6 +19,21 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: `${NOTIFICATIONS_RECITER_SUGGESTION_PATH}`,
|
||||
component: AdminNotificationsSuggestionTargetsPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
reciterSuggestionTargetParams: AdminNotificationsSuggestionTargetsPageResolver
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notifications.recitersuggestion.page.title',
|
||||
breadcrumbKey: 'admin.notifications.recitersuggestion',
|
||||
showBreadcrumbsFluid: false
|
||||
}
|
||||
},
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
|
||||
@@ -67,10 +85,11 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService,
|
||||
AdminNotificationsSuggestionTargetsPageResolver,
|
||||
SourceDataResolver,
|
||||
AdminQualityAssuranceSourcePageResolver,
|
||||
AdminQualityAssuranceTopicsPageResolver,
|
||||
AdminQualityAssuranceEventsPageResolver,
|
||||
AdminQualityAssuranceSourcePageResolver
|
||||
]
|
||||
})
|
||||
/**
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Interface for the route parameters.
|
||||
*/
|
||||
export interface AdminNotificationsSuggestionTargetsPageParams {
|
||||
pageId?: string;
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a resolver that retrieve the route data before the route is activated.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AdminNotificationsSuggestionTargetsPageResolver implements Resolve<AdminNotificationsSuggestionTargetsPageParams> {
|
||||
|
||||
/**
|
||||
* Method for resolving the parameters in the current route.
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsSuggestionTargetsPageParams {
|
||||
return {
|
||||
pageId: route.queryParams.pageId,
|
||||
pageSize: parseInt(route.queryParams.pageSize, 10),
|
||||
currentPage: parseInt(route.queryParams.page, 10)
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<ds-suggestion-target [source]="'oaire'"></ds-suggestion-target>
|
@@ -0,0 +1,38 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AdminNotificationsSuggestionTargetsPageComponent', () => {
|
||||
let component: AdminNotificationsSuggestionTargetsPageComponent;
|
||||
let fixture: ComponentFixture<AdminNotificationsSuggestionTargetsPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
AdminNotificationsSuggestionTargetsPageComponent
|
||||
],
|
||||
providers: [
|
||||
AdminNotificationsSuggestionTargetsPageComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminNotificationsSuggestionTargetsPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notifications-reciter-page',
|
||||
templateUrl: './admin-notifications-suggestion-targets-page.component.html',
|
||||
styleUrls: ['./admin-notifications-suggestion-targets-page.component.scss']
|
||||
})
|
||||
export class AdminNotificationsSuggestionTargetsPageComponent {
|
||||
|
||||
}
|
@@ -3,6 +3,7 @@ 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 { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-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';
|
||||
@@ -17,6 +18,7 @@ import {SuggestionNotificationsModule} from '../../suggestion-notifications/sugg
|
||||
SuggestionNotificationsModule
|
||||
],
|
||||
declarations: [
|
||||
AdminNotificationsSuggestionTargetsPageComponent,
|
||||
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';
|
||||
|
||||
@@ -202,6 +203,11 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
||||
.then((m) => m.ProcessPageModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{ path: SUGGESTION_MODULE_PATH,
|
||||
loadChildren: () => import('./suggestions-page/suggestions-page.module')
|
||||
.then((m) => m.SuggestionsPageModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{
|
||||
path: INFO_MODULE_PATH,
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
||||
|
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);
|
||||
|
||||
|
748
src/app/core/data/data.service.ts
Normal file
748
src/app/core/data/data.service.ts
Normal file
@@ -0,0 +1,748 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { AsyncSubject, combineLatest, from as observableFrom, Observable, of as observableOf } from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
find,
|
||||
map,
|
||||
mergeMap,
|
||||
skipWhile,
|
||||
switchMap,
|
||||
take,
|
||||
takeWhile,
|
||||
tap,
|
||||
toArray
|
||||
} from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { getClassForType } from '../cache/builders/build-decorators';
|
||||
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 { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import {
|
||||
CreateRequest,
|
||||
DeleteByIDRequest,
|
||||
DeleteRequest,
|
||||
GetRequest,
|
||||
PatchRequest,
|
||||
PostRequest,
|
||||
PutRequest
|
||||
} from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { UpdateDataService } from './update-data.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { FindListOptions } from './find-list-options.model';
|
||||
|
||||
export abstract class DataService<T extends CacheableObject> implements UpdateDataService<T> {
|
||||
protected abstract requestService: RequestService;
|
||||
protected abstract rdbService: RemoteDataBuildService;
|
||||
protected abstract store: Store<CoreState>;
|
||||
protected abstract linkPath: string;
|
||||
protected abstract halService: HALEndpointService;
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
protected abstract notificationsService: NotificationsService;
|
||||
protected abstract http: HttpClient;
|
||||
protected abstract comparator: ChangeAnalyzer<T>;
|
||||
|
||||
/**
|
||||
* Allows subclasses to reset the response cache time.
|
||||
*/
|
||||
protected responseMsToLive: number;
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @param linkPath The link path for the object
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.getEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base endpoint for all requests
|
||||
*/
|
||||
protected getEndpoint(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
let endpoint$: Observable<string>;
|
||||
const args = [];
|
||||
|
||||
endpoint$ = this.getBrowseEndpoint(options).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...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> {
|
||||
let result$: Observable<string>;
|
||||
const args = [];
|
||||
|
||||
result$ = this.getSearchEndpoint(searchMethod);
|
||||
|
||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an options object into a query string and combine it with the given HREF
|
||||
*
|
||||
* @param href The HREF to which the query string should be appended
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @param extraArgs Array with additional params to combine with query string
|
||||
* @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 buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||
let args = [...extraArgs];
|
||||
|
||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
||||
args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`);
|
||||
}
|
||||
if (hasValue(options.elementsPerPage)) {
|
||||
args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`);
|
||||
}
|
||||
if (hasValue(options.sort)) {
|
||||
args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`);
|
||||
}
|
||||
if (hasValue(options.startsWith)) {
|
||||
args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`);
|
||||
}
|
||||
if (hasValue(options.searchParams)) {
|
||||
options.searchParams.forEach((param: RequestParam) => {
|
||||
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
||||
});
|
||||
}
|
||||
args = this.addEmbedParams(href, args, ...linksToFollow);
|
||||
if (isNotEmpty(args)) {
|
||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an array of RequestParam into a query string and combine it with the given HREF
|
||||
*
|
||||
* @param href The HREF to which the query string should be appended
|
||||
* @param params Array with additional params to combine with query string
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
*/
|
||||
buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||
|
||||
let args = [];
|
||||
if (hasValue(params)) {
|
||||
params.forEach((param: RequestParam) => {
|
||||
args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`);
|
||||
});
|
||||
}
|
||||
|
||||
args = this.addEmbedParams(href, args, ...linksToFollow);
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds the embed options to the link for the request
|
||||
* @param href The href the params are to be added to
|
||||
* @param args params for the query string
|
||||
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||
*/
|
||||
protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]) {
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
||||
const embedString = 'embed=' + String(linkToFollow.name);
|
||||
// Add the embeds size if given in the FollowLinkConfig.FindListOptions
|
||||
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
||||
args = this.addHrefArg(href, args,
|
||||
'embed.size=' + String(linkToFollow.name) + '=' + linkToFollow.findListOptions.elementsPerPage);
|
||||
}
|
||||
// Adds the nested embeds and their size if given
|
||||
if (isNotEmpty(linkToFollow.linksToFollow)) {
|
||||
args = this.addNestedEmbeds(embedString, href, args, ...linkToFollow.linksToFollow);
|
||||
} else {
|
||||
args = this.addHrefArg(href, args, embedString);
|
||||
}
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new argument to the list of arguments, only if it doesn't already exist in the given href,
|
||||
* or the current list of arguments
|
||||
*
|
||||
* @param href The href the arguments are to be added to
|
||||
* @param currentArgs The current list of arguments
|
||||
* @param newArg The new argument to add
|
||||
* @return The next list of arguments, with newArg included if it wasn't already.
|
||||
* Note this function will not modify any of the input params.
|
||||
*/
|
||||
protected addHrefArg(href: string, currentArgs: string[], newArg: string): string[] {
|
||||
if (href.includes(newArg) || currentArgs.includes(newArg)) {
|
||||
return [...currentArgs];
|
||||
} else {
|
||||
return [...currentArgs, newArg];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the nested followLinks to the embed param, separated by a /, and their sizes, recursively
|
||||
* @param embedString embedString so far (recursive)
|
||||
* @param href The href the params are to be added to
|
||||
* @param args params for the query string
|
||||
* @param linksToFollow links we want to embed in query string if shouldEmbed is true
|
||||
*/
|
||||
protected addNestedEmbeds(embedString: string, href: string, args: string[], ...linksToFollow: FollowLinkConfig<T>[]): string[] {
|
||||
let nestEmbed = embedString;
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
||||
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
|
||||
// Add the nested embeds size if given in the FollowLinkConfig.FindListOptions
|
||||
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
||||
const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage;
|
||||
args = this.addHrefArg(href, args, nestedEmbedSize);
|
||||
}
|
||||
if (hasValue(linkToFollow.linksToFollow) && isNotEmpty(linkToFollow.linksToFollow)) {
|
||||
args = this.addNestedEmbeds(nestEmbed, href, args, ...linkToFollow.linksToFollow);
|
||||
} else {
|
||||
args = this.addHrefArg(href, args, nestEmbed);
|
||||
}
|
||||
}
|
||||
});
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow
|
||||
* @param endpoint The base endpoint for the type of object
|
||||
* @param resourceID The identifier for the object
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig<T>[]): string {
|
||||
return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable for the HREF of a specific object based on its identifier
|
||||
* @param resourceID The identifier for the object
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
getIDHrefObs(resourceID: string, ...linksToFollow: FollowLinkConfig<T>[]): Observable<string> {
|
||||
return this.getEndpoint().pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
|
||||
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||
* @param id ID of object we want to retrieve
|
||||
* @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
|
||||
*/
|
||||
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||
const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
|
||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* An operator that will call the given function if the incoming RemoteData is stale and
|
||||
* shouldReRequest is true
|
||||
*
|
||||
* @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale
|
||||
* @param requestFn The function to call if the RemoteData is stale and shouldReRequest is
|
||||
* true
|
||||
*/
|
||||
protected reRequestStaleRemoteData<O>(shouldReRequest: boolean, requestFn: () => Observable<RemoteData<O>>) {
|
||||
return (source: Observable<RemoteData<O>>): Observable<RemoteData<O>> => {
|
||||
if (shouldReRequest === true) {
|
||||
return source.pipe(
|
||||
tap((remoteData: RemoteData<O>) => {
|
||||
if (hasValue(remoteData) && remoteData.isStale) {
|
||||
requestFn();
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of
|
||||
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||
* @param href$ The url of object we want to retrieve. Can be a string or
|
||||
* an Observable<string>
|
||||
* @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
|
||||
*/
|
||||
findByHref(href$: string | Observable<string>, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||
if (typeof href$ === 'string') {
|
||||
href$ = observableOf(href$);
|
||||
}
|
||||
|
||||
const requestHref$ = href$.pipe(
|
||||
isNotEmptyOperator(),
|
||||
take(1),
|
||||
map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow))
|
||||
);
|
||||
|
||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||
|
||||
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
||||
// This skip ensures that if a stale object is present in the cache when you do a
|
||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||
// cached completed object
|
||||
skipWhile((rd: RemoteData<T>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list
|
||||
* of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||
* @param href$ The url of object we want to retrieve. Can be a string or
|
||||
* an Observable<string>
|
||||
* @param findListOptions 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
|
||||
*/
|
||||
findAllByHref(href$: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||
if (typeof href$ === 'string') {
|
||||
href$ = observableOf(href$);
|
||||
}
|
||||
|
||||
const requestHref$ = href$.pipe(
|
||||
isNotEmptyOperator(),
|
||||
take(1),
|
||||
map((href: string) => this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow))
|
||||
);
|
||||
|
||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||
|
||||
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
||||
// This skip ensures that if a stale object is present in the cache when you do a
|
||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||
// cached completed object
|
||||
skipWhile((rd: RemoteData<PaginatedList<T>>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
this.findAllByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GET request for the given href, and send it.
|
||||
*
|
||||
* @param href$ The url of object we want to retrieve. Can be a string or
|
||||
* an Observable<string>
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
*/
|
||||
protected createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable = true): void {
|
||||
if (isNotEmpty(href$)) {
|
||||
if (typeof href$ === 'string') {
|
||||
href$ = observableOf(href$);
|
||||
}
|
||||
|
||||
href$.pipe(
|
||||
isNotEmptyOperator(),
|
||||
take(1)
|
||||
).subscribe((href: string) => {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const request = new GetRequest(requestId, href);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return object search endpoint by given search method
|
||||
*
|
||||
* @param searchMethod The search method for the object
|
||||
*/
|
||||
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => `${href}/search/${searchMethod}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>>> {
|
||||
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||
|
||||
return this.findAllByHref(hrefObs, undefined, 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>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, object.uuid)));
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
).subscribe((href: string) => {
|
||||
const request = new PatchRequest(requestId, href, operations);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.send(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
createPatchFromCache(object: T): Observable<Operation[]> {
|
||||
const oldVersion$ = this.findByHref(object._links.self.href, true, false);
|
||||
return oldVersion$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((oldVersion: T) => this.comparator.diff(oldVersion, 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>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
||||
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
|
||||
this.requestService.send(request);
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.createPatchFromCache(object)
|
||||
.pipe(
|
||||
mergeMap((operations: Operation[]) => {
|
||||
if (isNotEmpty(operations)) {
|
||||
this.objectCache.addPatch(object._links.self.href, operations);
|
||||
}
|
||||
return this.findByHref(object._links.self.href, true, true);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.getEndpoint().pipe(
|
||||
isNotEmptyOperator(),
|
||||
distinctUntilChanged(),
|
||||
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
||||
);
|
||||
|
||||
const serializedObject = new DSpaceSerializer(getClassForType(object.type)).serialize(object);
|
||||
|
||||
endpoint$.pipe(
|
||||
take(1)
|
||||
).subscribe((endpoint: string) => {
|
||||
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedObject));
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.send(request);
|
||||
});
|
||||
|
||||
const result$ = this.rdbService.buildFromRequestUUID<T>(requestId);
|
||||
|
||||
// TODO a dataservice is not the best place to show a notification,
|
||||
// this should move up to the components that use this method
|
||||
result$.pipe(
|
||||
takeWhile((rd: RemoteData<T>) => rd.isLoading, true)
|
||||
).subscribe((rd: RemoteData<T>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1));
|
||||
}
|
||||
});
|
||||
|
||||
return result$;
|
||||
}
|
||||
|
||||
/**
|
||||
<<<<<<< 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.getIDHrefObs(objectId).pipe(
|
||||
switchMap((href: string) => this.deleteByHref(href, 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>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
if (copyVirtualMetadata) {
|
||||
copyVirtualMetadata.forEach((id) =>
|
||||
href += (href.includes('?') ? '&' : '?')
|
||||
+ 'copyVirtualMetadata='
|
||||
+ id
|
||||
);
|
||||
}
|
||||
|
||||
const request = new DeleteRequest(requestId, href);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.send(request);
|
||||
|
||||
const response$ = this.rdbService.buildFromRequestUUID(requestId);
|
||||
|
||||
const invalidated$ = new AsyncSubject<boolean>();
|
||||
response$.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((rd: RemoteData<NoContent>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
return this.invalidateByHref(href);
|
||||
} else {
|
||||
return [true];
|
||||
}
|
||||
})
|
||||
).subscribe(() => {
|
||||
invalidated$.next(true);
|
||||
invalidated$.complete();
|
||||
});
|
||||
|
||||
return combineLatest([response$, invalidated$]).pipe(
|
||||
filter(([_, invalidated]) => invalidated),
|
||||
map(([response, _]) => response),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit current object changes to the server
|
||||
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||
*/
|
||||
commitUpdates(method?: RestRequestMethod) {
|
||||
this.requestService.commit(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the links to traverse from the root of the api to the
|
||||
* endpoint this DataService 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
|
||||
* 'foo/bar'
|
||||
*/
|
||||
getLinkPath(): string {
|
||||
return this.linkPath;
|
||||
}
|
||||
}
|
@@ -1,46 +1,55 @@
|
||||
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 { 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';
|
||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
protected linkPath = 'workspaceitems';
|
||||
protected searchByItemLinkPath = 'item';
|
||||
|
||||
private searchData: SearchDataImpl<WorkspaceItem>;
|
||||
private deleteData: DeleteDataImpl<WorkspaceItem>;
|
||||
private deleteData: DeleteData<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);
|
||||
}
|
||||
|
||||
}
|
||||
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,64 +64,33 @@ 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;
|
||||
|
||||
/**
|
||||
* 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<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
|
||||
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`));
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, externalSourceEntryHref, options);
|
||||
this.requestService.send(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { ResourceType } from '../../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for the Suggestion Target object
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const SUGGESTION_TARGET = new ResourceType('suggestiontarget');
|
||||
|
||||
/**
|
||||
* The resource type for the Suggestion Source object
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const SUGGESTION_SOURCE = new ResourceType('suggestionsource');
|
||||
|
||||
/**
|
||||
* The resource type for the Suggestion object
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const SUGGESTION = new ResourceType('suggestion');
|
@@ -0,0 +1,47 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
|
||||
import { SUGGESTION_SOURCE } from './suggestion-objects.resource-type';
|
||||
import { excludeFromEquals } from '../../../utilities/equals.decorators';
|
||||
import { ResourceType } from '../../../shared/resource-type';
|
||||
import { HALLink } from '../../../shared/hal-link.model';
|
||||
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,61 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
|
||||
|
||||
import { CacheableObject } from '../../../cache/cacheable-object.model';
|
||||
import { SUGGESTION_TARGET } from './suggestion-objects.resource-type';
|
||||
import { excludeFromEquals } from '../../../utilities/equals.decorators';
|
||||
import { ResourceType } from '../../../shared/resource-type';
|
||||
import { HALLink } from '../../../shared/hal-link.model';
|
||||
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,85 @@
|
||||
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';
|
||||
|
||||
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,74 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { dataService } from '../../../data/base/data-service.decorator';
|
||||
import { SUGGESTION_SOURCE } from '../models/suggestion-objects.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';
|
||||
|
||||
@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.findAllData.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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,238 @@
|
||||
/* 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 { DataService } from '../../data/data.service';
|
||||
import { ChangeAnalyzer } from '../../data/change-analyzer';
|
||||
import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { SUGGESTION } 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 DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
class SuggestionDataServiceImpl extends DataService<Suggestion> {
|
||||
/**
|
||||
* The REST endpoint.
|
||||
*/
|
||||
protected linkPath = 'suggestions';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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>) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The service handling all Suggestion Target REST requests.
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(SUGGESTION)
|
||||
export class SuggestionsDataService {
|
||||
protected searchFindBySourceMethod = 'findBySource';
|
||||
protected searchFindByTargetMethod = 'findByTarget';
|
||||
protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource';
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
private suggestionsDataService: SuggestionDataServiceImpl;
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
private suggestionSourcesDataService: SuggestionSourceDataService;
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
private suggestionTargetsDataService: SuggestionTargetDataService;
|
||||
|
||||
/**
|
||||
* 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.suggestionSourcesDataService = new SuggestionSourceDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources);
|
||||
this.suggestionTargetsDataService = new SuggestionTargetDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorTargets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of Suggestion Target
|
||||
*
|
||||
* @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(this.searchFindByTargetMethod, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch Suggestion notification for user
|
||||
* @suggestionId
|
||||
*/
|
||||
public getSuggestion(suggestionId: string, ...linksToFollow: FollowLinkConfig<Suggestion>[]): Observable<RemoteData<Suggestion>> {
|
||||
return this.suggestionsDataService.findById(suggestionId, true, true, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of Suggestion for a given target and source
|
||||
*
|
||||
* @param target
|
||||
* The target for which to find suggestions.
|
||||
* @param source
|
||||
* The source for which to find suggestions.
|
||||
* @param options
|
||||
* Find list options object.
|
||||
* @param linksToFollow
|
||||
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||
* @return Observable<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, true, true, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear findByTargetAndSource suggestions requests from cache
|
||||
*/
|
||||
public clearSuggestionRequests() {
|
||||
this.requestService.setStaleByHrefSubstring(this.searchFindByTargetAndSourceMethod);
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { dataService } from '../../../data/base/data-service.decorator';
|
||||
import { SUGGESTION_TARGET } from '../models/suggestion-objects.resource-type';
|
||||
import { IdentifiableDataService } from '../../../data/base/identifiable-data.service';
|
||||
import { SuggestionTarget } from '../models/suggestion-target.model';
|
||||
import { FindAllData, FindAllDataImpl } from '../../../data/base/find-all-data';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
@dataService(SUGGESTION_TARGET)
|
||||
export class SuggestionTargetDataService extends IdentifiableDataService<SuggestionTarget> {
|
||||
|
||||
protected linkPath = 'suggestiontargets';
|
||||
private findAllData: FindAllData<SuggestionTarget>;
|
||||
private searchBy: SearchData<SuggestionTarget>;
|
||||
protected searchFindBySourceMethod = 'findBySource';
|
||||
protected searchFindByTargetMethod = 'findByTarget';
|
||||
protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource';
|
||||
|
||||
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.searchBy = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||
}
|
||||
/**
|
||||
* Return the list of Suggestion Target for a given source
|
||||
*
|
||||
* @param source
|
||||
* The source for which to find targets.
|
||||
* @param options
|
||||
* Find list options object.
|
||||
* @param linksToFollow
|
||||
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||
* @return Observable<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.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow);
|
||||
}
|
||||
/**
|
||||
* Return a single Suggestion target.
|
||||
*
|
||||
* @param id The Suggestion Target id
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||
*
|
||||
* @return Observable<RemoteData<QualityAssuranceSourceObject>> The Quality Assurance source.
|
||||
*/
|
||||
public getTarget(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<SuggestionTarget>[]): Observable<RemoteData<SuggestionTarget>> {
|
||||
return this.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of Suggestion Target for a given user
|
||||
*
|
||||
* @param userId
|
||||
* The user Id for which to find targets.
|
||||
* @param options
|
||||
* Find list options object.
|
||||
* @param linksToFollow
|
||||
* List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved.
|
||||
* @return Observable<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.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);
|
||||
}
|
||||
|
||||
}
|
42
src/app/shared/mocks/reciter-suggestion-targets.mock.ts
Normal file
42
src/app/shared/mocks/reciter-suggestion-targets.mock.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/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/reciter-suggestion.mock.ts
Normal file
210
src/app/shared/mocks/reciter-suggestion.mock.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
// REST Mock ---------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
import { Suggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
import { SUGGESTION } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-objects.resource-type';
|
||||
|
||||
export const mockSuggestionPublicationOne: Suggestion = {
|
||||
id: '24694772',
|
||||
display: 'publication one',
|
||||
source: 'reciter',
|
||||
externalSourceUri: 'https://dspace7.4science.cloud/server/api/integration/reciterSourcesEntry/pubmed/entryValues/24694772',
|
||||
score: '48',
|
||||
evidences: {
|
||||
acceptedRejectedEvidence: {
|
||||
score: '2.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
authorNameEvidence: {
|
||||
score: '0',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
journalCategoryEvidence: {
|
||||
score: '6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
affiliationEvidence: {
|
||||
score: 'xxx',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
relationshipEvidence: {
|
||||
score: '9',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
educationYearEvidence: {
|
||||
score: '3.6',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
personTypeEvidence: {
|
||||
score: '4',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
articleCountEvidence: {
|
||||
score: '6.7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
},
|
||||
averageClusteringEvidence: {
|
||||
score: '7',
|
||||
notes: 'some notes, eventually empty or null'
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: 'https://publication/0000-0003-3681-2038',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1,
|
||||
place: -1
|
||||
} as any
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'publication one',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
value: '2010-11-03',
|
||||
language: null,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dspace.entity.type': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: 'OrgUnit',
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
],
|
||||
'dc.description': [
|
||||
{
|
||||
uuid: '95f21fe6-ce38-43d6-96d4-60ae66385a06',
|
||||
language: null,
|
||||
value: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).",
|
||||
place: 0,
|
||||
authority: null,
|
||||
confidence: -1
|
||||
} as any
|
||||
]
|
||||
},
|
||||
type: SUGGESTION,
|
||||
_links: {
|
||||
target: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/core/items/gf3d657-9d6d-4a87-b905-fef0f8cae26'
|
||||
},
|
||||
self: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/suggestions/reciter:gf3d657-9d6d-4a87-b905-fef0f8cae26c:24694772'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mockSuggestionPublicationTwo: 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'
|
||||
}
|
||||
}
|
||||
};
|
1352
src/app/shared/mocks/suggestion.mock.ts
Normal file
1352
src/app/shared/mocks/suggestion.mock.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
import {createFeatureSelector, createSelector, MemoizedSelector} from '@ngrx/store';
|
||||
import { suggestionNotificationsSelector, SuggestionNotificationsState } from '../suggestion-notifications.reducer';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer';
|
||||
import {subStateSelector} from '../../submission/selectors';
|
||||
|
||||
/**
|
||||
* Returns the Reciter Suggestion Target state.
|
||||
* @function _getReciterSuggestionTargetState
|
||||
* @param {AppState} state Top level state.
|
||||
* @return {SuggestionNotificationsState}
|
||||
*/
|
||||
const _getReciterSuggestionTargetState = createFeatureSelector<SuggestionNotificationsState>('suggestionNotifications');
|
||||
|
||||
// Reciter Suggestion Targets
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the Reciter Suggestion Targets State.
|
||||
* @function reciterSuggestionTargetStateSelector
|
||||
* @return {SuggestionNotificationsState}
|
||||
*/
|
||||
export function reciterSuggestionTargetStateSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTargetState> {
|
||||
return subStateSelector<SuggestionNotificationsState, SuggestionTargetState>(suggestionNotificationsSelector, 'suggestionTarget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Reciter Suggestion Targets list.
|
||||
* @function reciterSuggestionTargetObjectSelector
|
||||
* @return {OpenaireReciterSuggestionTarget[]}
|
||||
*/
|
||||
export function reciterSuggestionTargetObjectSelector(): MemoizedSelector<SuggestionNotificationsState, SuggestionTarget[]> {
|
||||
return subStateSelector<SuggestionNotificationsState, SuggestionTarget[]>(reciterSuggestionTargetStateSelector(), 'targets');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Reciter Suggestion Targets are loaded.
|
||||
* @function isReciterSuggestionTargetLoadedSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isReciterSuggestionTargetLoadedSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.loaded
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the deduplication sets are processing.
|
||||
* @function isDeduplicationSetsProcessingSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isreciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.processing
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the total available pages of Reciter Suggestion Targets.
|
||||
* @function getreciterSuggestionTargetTotalPagesSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.totalPages
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the current page of Reciter Suggestion Targets.
|
||||
* @function getreciterSuggestionTargetCurrentPageSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentPage
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the total number of Reciter Suggestion Targets.
|
||||
* @function getreciterSuggestionTargetTotalsSelector
|
||||
* @return {number}
|
||||
*/
|
||||
export const getreciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.totalElements
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns Suggestion Targets for the current user.
|
||||
* @function getCurrentUserReciterSuggestionTargetSelector
|
||||
* @return {SuggestionTarget[]}
|
||||
*/
|
||||
export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns whether or not the user has consulted their suggestions
|
||||
* @function getCurrentUserReciterSuggestionTargetSelector
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getReciterSuggestionTargetState,
|
||||
(state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited
|
||||
);
|
@@ -0,0 +1,28 @@
|
||||
<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)="notMine()" class="btn btn-danger ml-2"><i class="fa fa-ban"></i>
|
||||
{{ notMineLabel() | translate}}</button>
|
||||
<button *ngIf="!isBulk" (click)="toggleSeeEvidences()" [disabled]="!hasEvidence" class="btn btn-info mt-1 ml-2"><i class="fa fa-eye"></i>
|
||||
<ng-container *ngIf="!seeEvidence">{{'reciter.suggestion.seeEvidence' | translate}}</ng-container>
|
||||
<ng-container *ngIf="seeEvidence">{{'reciter.suggestion.hideEvidence' | translate}}</ng-container>
|
||||
</button>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,92 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Suggestion } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-list-element.component';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { CreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestion-actions',
|
||||
styleUrls: [ './suggestion-actions.component.scss' ],
|
||||
templateUrl: './suggestion-actions.component.html'
|
||||
})
|
||||
export class SuggestionActionsComponent {
|
||||
|
||||
@Input() object: Suggestion;
|
||||
|
||||
@Input() isBulk = false;
|
||||
|
||||
@Input() hasEvidence = false;
|
||||
|
||||
@Input() seeEvidence = false;
|
||||
|
||||
@Input() isCollectionFixed = false;
|
||||
|
||||
/**
|
||||
* The component is used to Delete suggestion
|
||||
*/
|
||||
@Output() notMineClicked = 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
|
||||
*/
|
||||
notMine() {
|
||||
this.notMineClicked.emit(this.isBulk ? undefined : this.object.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle See Evidence
|
||||
*/
|
||||
toggleSeeEvidences() {
|
||||
this.seeEvidences.emit(!this.seeEvidence);
|
||||
}
|
||||
|
||||
notMineLabel(): string {
|
||||
return this.isBulk ? 'reciter.suggestion.notMine.bulk' : 'reciter.suggestion.notMine' ;
|
||||
}
|
||||
|
||||
approveAndImportLabel(): string {
|
||||
return this.isBulk ? 'reciter.suggestion.approveAndImport.bulk' : 'reciter.suggestion.approveAndImport';
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<div class="table-responsive" *ngIf="evidences">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{'reciter.suggestion.evidence.score' | translate}}</th>
|
||||
<th>{{'reciter.suggestion.evidence.type' | translate}}</th>
|
||||
<th>{{'reciter.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,15 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { fadeIn } from '../../../../shared/animations/fade';
|
||||
import { SuggestionEvidences } from '../../../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestion-evidences',
|
||||
styleUrls: [ './suggestion-evidences.component.scss' ],
|
||||
templateUrl: './suggestion-evidences.component.html',
|
||||
animations: [fadeIn]
|
||||
})
|
||||
export class SuggestionEvidencesComponent {
|
||||
|
||||
@Input() evidences: SuggestionEvidences;
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
<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)"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Total Score Panel -->
|
||||
<div class="col-2 text-center align-self-center">
|
||||
<div class="">
|
||||
<div><strong> {{'reciter.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"
|
||||
[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)"
|
||||
(notMineClicked)="onNotMine($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,98 @@
|
||||
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/reciter-suggestions/models/suggestion.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
|
||||
export interface SuggestionApproveAndImport {
|
||||
suggestion: Suggestion;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestion-list-item',
|
||||
styleUrls: ['./suggestion-list-element.component.scss'],
|
||||
templateUrl: './suggestion-list-element.component.html',
|
||||
animations: [fadeIn]
|
||||
})
|
||||
export class SuggestionListElementComponent implements OnInit {
|
||||
|
||||
@Input() object: Suggestion;
|
||||
|
||||
@Input() isSelected = false;
|
||||
|
||||
@Input() isCollectionFixed = false;
|
||||
|
||||
public listableObject: any;
|
||||
|
||||
public seeEvidence = false;
|
||||
|
||||
/**
|
||||
* The component is used to Delete suggestion
|
||||
*/
|
||||
@Output() notMineClicked = new EventEmitter();
|
||||
|
||||
/**
|
||||
* The component is used to approve & import
|
||||
*/
|
||||
@Output() approveAndImport = new EventEmitter();
|
||||
|
||||
/**
|
||||
* New value whether the element is selected
|
||||
*/
|
||||
@Output() selected = new EventEmitter<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
|
||||
*/
|
||||
onNotMine(suggestionId: string) {
|
||||
this.notMineClicked.emit(suggestionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change is selected value.
|
||||
*/
|
||||
changeSelected(event) {
|
||||
this.isSelected = event.target.checked;
|
||||
this.selected.next(this.isSelected);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the Evidence
|
||||
*/
|
||||
hasEvidences() {
|
||||
return isNotEmpty(this.object.evidences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the see evidence variable.
|
||||
*/
|
||||
onSeeEvidences(seeEvidence: boolean) {
|
||||
this.seeEvidence = seeEvidence;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/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;
|
@@ -0,0 +1,50 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h3 id="header" class="border-bottom pb-2">{{'reciter.suggestion.title'| translate}}</h3>
|
||||
|
||||
<ds-loading class="container" *ngIf="(isTargetsLoading() | async)" message="{{'reciter.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="'reciter.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">
|
||||
{{'reciter.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">{{'reciter.suggestion.table.name' | translate}}</th>
|
||||
<th scope="col">{{'reciter.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]="['/items', getTargetUuid(targetElement)]">{{targetElement.display}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="redirectToSuggestions(targetElement.id, targetElement.display)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{'reciter.suggestion.button.review' | translate }}">
|
||||
<span >{{'reciter.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,150 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged, take } from 'rxjs/operators';
|
||||
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SuggestionTargetsStateService } from './suggestion-targets.state.service';
|
||||
import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Component to display the Suggestion Target list.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-suggestion-target',
|
||||
templateUrl: './suggestion-targets.component.html',
|
||||
styleUrls: ['./suggestion-targets.component.scss'],
|
||||
})
|
||||
export class SuggestionTargetsComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The source for which to list targets
|
||||
*/
|
||||
@Input() source: string;
|
||||
|
||||
/**
|
||||
* The pagination system configuration for HTML listing.
|
||||
* @type {PaginationComponentOptions}
|
||||
*/
|
||||
public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'stp',
|
||||
pageSizeOptions: [5, 10, 20, 40, 60]
|
||||
});
|
||||
|
||||
/**
|
||||
* The Suggestion Target list.
|
||||
*/
|
||||
public targets$: Observable<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.getReciterSuggestionTargets();
|
||||
this.totalElements$ = this.suggestionTargetsStateService.getReciterSuggestionTargetsTotals();
|
||||
}
|
||||
|
||||
/**
|
||||
* First Suggestion Targets loading after view initialization.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
this.subs.push(
|
||||
this.suggestionTargetsStateService.isReciterSuggestionTargetsLoaded().pipe(
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.getSuggestionTargets();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loading, 'false' otherwise.
|
||||
*/
|
||||
public isTargetsLoading(): Observable<boolean> {
|
||||
return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.isReciterSuggestionTargetsProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to suggestion page.
|
||||
*
|
||||
* @param {string} id
|
||||
* the id of suggestion target
|
||||
* @param {string} name
|
||||
* the name of suggestion target
|
||||
*/
|
||||
public redirectToSuggestions(id: string, name: string) {
|
||||
this.router.navigate([getSuggestionPageRoute(id)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction();
|
||||
this.subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the Suggestion Targets retrival.
|
||||
*/
|
||||
public getSuggestionTargets(): void {
|
||||
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
|
||||
distinctUntilChanged(),
|
||||
take(1)
|
||||
).subscribe((options: PaginationComponentOptions) => {
|
||||
this.suggestionTargetsStateService.dispatchRetrieveReciterSuggestionTargets(
|
||||
this.source,
|
||||
options.pageSize,
|
||||
options.currentPage
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getTargetUuid(target: SuggestionTarget) {
|
||||
return this.suggestionService.getTargetUuid(target);
|
||||
}
|
||||
}
|
@@ -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,
|
||||
RefreshUserSuggestionsAction,
|
||||
RetrieveAllTargetsErrorAction,
|
||||
RetrieveTargetsBySourceAction,
|
||||
SuggestionTargetActionTypes,
|
||||
} from './suggestion-targets.actions';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/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('reciter.suggestion.target.error.service.retrieve'));
|
||||
})
|
||||
), { dispatch: false });
|
||||
|
||||
/**
|
||||
* Fetch the current user suggestion
|
||||
*/
|
||||
refreshUserTargets$ = createEffect(() => this.actions$.pipe(
|
||||
ofType(SuggestionTargetActionTypes.REFRESH_USER_SUGGESTIONS),
|
||||
switchMap((action: RefreshUserSuggestionsAction) => {
|
||||
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/reciter-suggestions/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,
|
||||
getreciterSuggestionTargetCurrentPageSelector,
|
||||
getreciterSuggestionTargetTotalsSelector,
|
||||
isReciterSuggestionTargetLoadedSelector,
|
||||
isreciterSuggestionTargetProcessingSelector,
|
||||
reciterSuggestionTargetObjectSelector
|
||||
} from '../selectors';
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import {
|
||||
ClearSuggestionTargetsAction,
|
||||
MarkUserSuggestionsAsVisitedAction,
|
||||
RefreshUserSuggestionsAction,
|
||||
RetrieveTargetsBySourceAction
|
||||
} from './suggestion-targets.actions';
|
||||
import { SuggestionNotificationsState } from '../../suggestion-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 Reciter Suggestion Targets from the state.
|
||||
*
|
||||
* @return Observable<OpenaireReciterSuggestionTarget>
|
||||
* The list of Reciter Suggestion Targets.
|
||||
*/
|
||||
public getReciterSuggestionTargets(): Observable<SuggestionTarget[]> {
|
||||
return this.store.pipe(select(reciterSuggestionTargetObjectSelector()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Reciter Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loading, 'false' otherwise.
|
||||
*/
|
||||
public isReciterSuggestionTargetsLoading(): Observable<boolean> {
|
||||
return this.store.pipe(
|
||||
select(isReciterSuggestionTargetLoadedSelector),
|
||||
map((loaded: boolean) => !loaded)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the loading status of the Reciter Suggestion Targets (whether or not they were loaded).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if the targets are loaded, 'false' otherwise.
|
||||
*/
|
||||
public isReciterSuggestionTargetsLoaded(): Observable<boolean> {
|
||||
return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information about the processing status of the Reciter Suggestion Targets (if it's running or not).
|
||||
*
|
||||
* @return Observable<boolean>
|
||||
* 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
|
||||
*/
|
||||
public isReciterSuggestionTargetsProcessing(): Observable<boolean> {
|
||||
return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, from the state, the total available pages of the Reciter Suggestion Targets.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the Reciter Suggestion Targets pages.
|
||||
*/
|
||||
public getReciterSuggestionTargetsTotalPages(): Observable<number> {
|
||||
return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current page of the Reciter Suggestion Targets, from the state.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the current Reciter Suggestion Targets page.
|
||||
*/
|
||||
public getReciterSuggestionTargetsCurrentPage(): Observable<number> {
|
||||
return this.store.pipe(select(getreciterSuggestionTargetCurrentPageSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of the Reciter Suggestion Targets.
|
||||
*
|
||||
* @return Observable<number>
|
||||
* The number of the Reciter Suggestion Targets.
|
||||
*/
|
||||
public getReciterSuggestionTargetsTotals(): Observable<number> {
|
||||
return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a request to change the Reciter Suggestion Targets state, retrieving the targets from the server.
|
||||
*
|
||||
* @param source
|
||||
* the source for which to retrieve suggestion targets
|
||||
* @param elementsPerPage
|
||||
* The number of the targets per page.
|
||||
* @param currentPage
|
||||
* The number of the current page.
|
||||
*/
|
||||
public dispatchRetrieveReciterSuggestionTargets(source: string, elementsPerPage: number, currentPage: number): void {
|
||||
this.store.dispatch(new RetrieveTargetsBySourceAction(source, elementsPerPage, currentPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, from the state, the reciter suggestion targets for the current user.
|
||||
*
|
||||
* @return Observable<OpenaireReciterSuggestionTarget>
|
||||
* The Reciter 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());
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
<ng-container *ngIf="(suggestionsRD$ | async) as suggestions">
|
||||
<ng-container *ngFor="let suggestion of suggestions" class="alert alert-info">
|
||||
<div class="alert alert-success d-block" *ngIf="suggestion.total > 0">
|
||||
<div [innerHTML]="'mydspace.notification.suggestion.page' | translate: getNotificationSuggestionInterpolation(suggestion)">
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
@@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestions-notification',
|
||||
templateUrl: './suggestions-notification.component.html',
|
||||
styleUrls: ['./suggestions-notification.component.scss']
|
||||
})
|
||||
export class SuggestionsNotificationComponent implements OnInit {
|
||||
|
||||
labelPrefix = 'mydspace.';
|
||||
|
||||
/**
|
||||
* The user suggestion targets.
|
||||
*/
|
||||
suggestionsRD$: Observable<SuggestionTarget[]>;
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
private reciterSuggestionStateService: SuggestionTargetsStateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private suggestionsService: SuggestionsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.suggestionsRD$ = this.reciterSuggestionStateService.getCurrentUserSuggestionTargets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolated params to build the notification suggestions notification.
|
||||
* @param suggestionTarget
|
||||
*/
|
||||
public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any {
|
||||
return this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SuggestionsPopupComponent } from './suggestions-popup.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { mockSuggestionTargetsObjectOne } from '../../../shared/mocks/reciter-suggestion-targets.mock';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
|
||||
describe('SuggestionsPopupComponent', () => {
|
||||
let component: SuggestionsPopupComponent;
|
||||
let fixture: ComponentFixture<SuggestionsPopupComponent>;
|
||||
|
||||
const suggestionStateService = jasmine.createSpyObj('SuggestionTargetsStateService', {
|
||||
hasUserVisitedSuggestions: jasmine.createSpy('hasUserVisitedSuggestions'),
|
||||
getCurrentUserSuggestionTargets: jasmine.createSpy('getCurrentUserSuggestionTargets'),
|
||||
dispatchMarkUserSuggestionsAsVisitedAction: jasmine.createSpy('dispatchMarkUserSuggestionsAsVisitedAction')
|
||||
});
|
||||
|
||||
const mockNotificationInterpolation = { count: 12, source: 'source', suggestionId: 'id', displayName: 'displayName' };
|
||||
const suggestionService = jasmine.createSpyObj('SuggestionService', {
|
||||
getNotificationSuggestionInterpolation:
|
||||
jasmine.createSpy('getNotificationSuggestionInterpolation').and.returnValue(mockNotificationInterpolation)
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ SuggestionsPopupComponent ],
|
||||
providers: [
|
||||
{ provide: SuggestionTargetsStateService, useValue: suggestionStateService },
|
||||
{ provide: SuggestionsService, useValue: suggestionService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
describe('should create', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SuggestionsPopupComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component, 'initializePopup').and.returnValue(null);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.initializePopup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when there are publication suggestions', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
suggestionStateService.hasUserVisitedSuggestions.and.returnValue(observableOf(false));
|
||||
suggestionStateService.getCurrentUserSuggestionTargets.and.returnValue(observableOf([mockSuggestionTargetsObjectOne]));
|
||||
suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction.and.returnValue(observableOf(null));
|
||||
|
||||
fixture = TestBed.createComponent(SuggestionsPopupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show a notification when new publication suggestions are available', () => {
|
||||
expect((component as any).notificationsService.success).toHaveBeenCalled();
|
||||
expect(suggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -0,0 +1,67 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { SuggestionsService } from '../suggestions.service';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { SuggestionTarget } from '../../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { combineLatest, Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestions-popup',
|
||||
templateUrl: './suggestions-popup.component.html',
|
||||
styleUrls: ['./suggestions-popup.component.scss']
|
||||
})
|
||||
export class SuggestionsPopupComponent implements OnInit, OnDestroy {
|
||||
|
||||
labelPrefix = 'mydspace.';
|
||||
|
||||
subscription;
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
private reciterSuggestionStateService: SuggestionTargetsStateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private suggestionsService: SuggestionsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.initializePopup();
|
||||
}
|
||||
|
||||
public initializePopup() {
|
||||
const notifier = new Subject();
|
||||
this.subscription = combineLatest([
|
||||
this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(),
|
||||
this.reciterSuggestionStateService.hasUserVisitedSuggestions()
|
||||
]).pipe(takeUntil(notifier)).subscribe(([suggestions, visited]) => {
|
||||
if (isNotEmpty(suggestions)) {
|
||||
if (!visited) {
|
||||
suggestions.forEach((suggestionTarget: SuggestionTarget) => this.showNotificationForNewSuggestions(suggestionTarget));
|
||||
this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction();
|
||||
notifier.next(null);
|
||||
notifier.complete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification to user for a new suggestions detected
|
||||
* @param suggestionTarget
|
||||
* @private
|
||||
*/
|
||||
private showNotificationForNewSuggestions(suggestionTarget: SuggestionTarget): void {
|
||||
const content = this.translateService.instant(this.labelPrefix + 'notification.suggestion',
|
||||
this.suggestionsService.getNotificationSuggestionInterpolation(suggestionTarget));
|
||||
this.notificationsService.success('', content, {timeOut:0}, true);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,306 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { of, forkJoin, Observable } from 'rxjs';
|
||||
import { catchError, map, mergeMap, take } from 'rxjs/operators';
|
||||
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { SuggestionTarget } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getFinishedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteListPayload
|
||||
} from '../../core/shared/operators';
|
||||
import { Suggestion } from '../../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
||||
import {FindListOptions} from '../../core/data/find-list-options.model';
|
||||
import {SuggestionConfig} from '../../../config/layout-config.interfaces';
|
||||
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
|
||||
import {
|
||||
SuggestionSourceDataService
|
||||
} from '../../core/suggestion-notifications/reciter-suggestions/source/suggestion-source-data.service';
|
||||
import {
|
||||
SuggestionTargetDataService
|
||||
} from '../../core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service';
|
||||
import {
|
||||
SuggestionsDataService
|
||||
} from '../../core/suggestion-notifications/reciter-suggestions/suggestions-data.service';
|
||||
|
||||
export interface SuggestionBulkResult {
|
||||
success: number;
|
||||
fails: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The service handling all Suggestion Target requests to the REST service.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SuggestionsService {
|
||||
|
||||
/**
|
||||
* Initialize the service variables.
|
||||
* @param {AuthService} authService
|
||||
* @param {ResearcherProfileService} researcherProfileService
|
||||
* @param {SuggestionsDataService} suggestionsDataService
|
||||
*/
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private researcherProfileService: ResearcherProfileDataService,
|
||||
private suggestionsDataService: SuggestionsDataService,
|
||||
private suggestionSourceDataService: SuggestionSourceDataService,
|
||||
private suggestionTargetDataService: SuggestionTargetDataService,
|
||||
private translateService: TranslateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of Suggestion Target managing pagination and errors.
|
||||
*
|
||||
* @param source
|
||||
* The source for which to retrieve targets
|
||||
* @param elementsPerPage
|
||||
* The number of the target per page
|
||||
* @param currentPage
|
||||
* The page number to retrieve
|
||||
* @return Observable<PaginatedList<OpenaireReciterSuggestionTarget>>
|
||||
* The list of Suggestion Targets.
|
||||
*/
|
||||
public getTargets(source, elementsPerPage, currentPage): Observable<PaginatedList<SuggestionTarget>> {
|
||||
const sortOptions = new SortOptions('display', SortDirection.ASC);
|
||||
|
||||
const findListOptions: FindListOptions = {
|
||||
elementsPerPage: elementsPerPage,
|
||||
currentPage: currentPage,
|
||||
sort: sortOptions
|
||||
};
|
||||
|
||||
return this.suggestionTargetDataService.getTargets(source, findListOptions).pipe(
|
||||
getFinishedRemoteData(),
|
||||
take(1),
|
||||
map((rd: RemoteData<PaginatedList<SuggestionTarget>>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
throw new Error('Can\'t retrieve Suggestion Target from the Search Target REST service');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of review suggestions Target managing pagination and errors.
|
||||
*
|
||||
* @param targetId
|
||||
* The target id for which to find suggestions.
|
||||
* @param elementsPerPage
|
||||
* The number of the target per page
|
||||
* @param currentPage
|
||||
* The page number to retrieve
|
||||
* @param sortOptions
|
||||
* The sort options
|
||||
* @return Observable<RemoteData<PaginatedList<Suggestion>>>
|
||||
* The list of Suggestion.
|
||||
*/
|
||||
public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable<PaginatedList<Suggestion>> {
|
||||
const [source, target] = targetId.split(':');
|
||||
|
||||
const findListOptions: FindListOptions = {
|
||||
elementsPerPage: elementsPerPage,
|
||||
currentPage: currentPage,
|
||||
sort: sortOptions
|
||||
};
|
||||
|
||||
return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions).pipe(
|
||||
getAllSucceededRemoteDataPayload()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear suggestions requests from cache
|
||||
*/
|
||||
public clearSuggestionRequests() {
|
||||
this.suggestionsDataService.clearSuggestionRequests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete Suggestion
|
||||
* @suggestionId
|
||||
*/
|
||||
public deleteReviewedSuggestion(suggestionId: string): Observable<RemoteData<NoContent>> {
|
||||
return this.suggestionsDataService.deleteSuggestion(suggestionId).pipe(
|
||||
map((response: RemoteData<NoContent>) => {
|
||||
if (response.isSuccess) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Can\'t delete Suggestion from the Search Target REST service');
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve suggestion targets for the given user
|
||||
*
|
||||
* @param userUuid
|
||||
* The EPerson id for which to retrieve suggestion targets
|
||||
*/
|
||||
public retrieveCurrentUserSuggestions(userUuid: string): Observable<SuggestionTarget[]> {
|
||||
return this.researcherProfileService.findById(userUuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
mergeMap((profile: ResearcherProfile) => {
|
||||
if (isNotEmpty(profile)) {
|
||||
return this.researcherProfileService.findRelatedItemId(profile).pipe(
|
||||
mergeMap((itemId: string) => {
|
||||
return this.suggestionsDataService.getTargetsByUser(itemId).pipe(
|
||||
getFirstSucceededRemoteListPayload()
|
||||
);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return of([]);
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the approve and import operation over a single suggestion
|
||||
* @param suggestion target suggestion
|
||||
* @param collectionId the collectionId
|
||||
* @param workspaceitemService injected dependency
|
||||
* @private
|
||||
*/
|
||||
public approveAndImport(workspaceitemService: WorkspaceitemDataService,
|
||||
suggestion: Suggestion,
|
||||
collectionId: string): Observable<WorkspaceItem> {
|
||||
|
||||
const resolvedCollectionId = this.resolveCollectionId(suggestion, collectionId);
|
||||
return workspaceitemService.importExternalSourceEntry(suggestion.externalSourceUri, resolvedCollectionId)
|
||||
.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
catchError((error) => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the delete operation over a single suggestion.
|
||||
* @param suggestionId
|
||||
*/
|
||||
public notMine(suggestionId): Observable<RemoteData<NoContent>> {
|
||||
return this.deleteReviewedSuggestion(suggestionId).pipe(
|
||||
catchError((error) => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a bulk approve and import operation.
|
||||
* @param workspaceitemService injected dependency
|
||||
* @param suggestions the array containing the suggestions
|
||||
* @param collectionId the collectionId
|
||||
*/
|
||||
public approveAndImportMultiple(workspaceitemService: WorkspaceitemDataService,
|
||||
suggestions: Suggestion[],
|
||||
collectionId: string): Observable<SuggestionBulkResult> {
|
||||
|
||||
return forkJoin(suggestions.map((suggestion: Suggestion) =>
|
||||
this.approveAndImport(workspaceitemService, suggestion, collectionId)))
|
||||
.pipe(map((results: WorkspaceItem[]) => {
|
||||
return {
|
||||
success: results.filter((result) => result != null).length,
|
||||
fails: results.filter((result) => result == null).length
|
||||
};
|
||||
}), take(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a bulk notMine operation.
|
||||
* @param suggestions the array containing the suggestions
|
||||
*/
|
||||
public notMineMultiple(suggestions: Suggestion[]): Observable<SuggestionBulkResult> {
|
||||
return forkJoin(suggestions.map((suggestion: Suggestion) => this.notMine(suggestion.id)))
|
||||
.pipe(map((results: RemoteData<NoContent>[]) => {
|
||||
return {
|
||||
success: results.filter((result) => result != null).length,
|
||||
fails: results.filter((result) => result == null).length
|
||||
};
|
||||
}), take(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the researcher uuid (for navigation purpose) from a target instance.
|
||||
* TODO Find a better way
|
||||
* @param target
|
||||
* @return the researchUuid
|
||||
*/
|
||||
public getTargetUuid(target: SuggestionTarget): string {
|
||||
const tokens = target.id.split(':');
|
||||
return tokens.length === 2 ? tokens[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolated params to build the notification suggestions notification.
|
||||
* @param suggestionTarget
|
||||
*/
|
||||
public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any {
|
||||
return {
|
||||
count: suggestionTarget.total,
|
||||
source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)),
|
||||
type: this.translateService.instant(this.translateSuggestionType(suggestionTarget.source)),
|
||||
suggestionId: suggestionTarget.id,
|
||||
displayName: suggestionTarget.display
|
||||
};
|
||||
}
|
||||
|
||||
public translateSuggestionType(source: string): string {
|
||||
return 'reciter.suggestion.type.' + source;
|
||||
}
|
||||
|
||||
public translateSuggestionSource(source: string): string {
|
||||
return 'reciter.suggestion.source.' + source;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided collectionId ha no value, tries to resolve it by suggestion source.
|
||||
* @param suggestion
|
||||
* @param collectionId
|
||||
*/
|
||||
public resolveCollectionId(suggestion: Suggestion, collectionId): string {
|
||||
if (hasValue(collectionId)) {
|
||||
return collectionId;
|
||||
}
|
||||
return environment.suggestion
|
||||
.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source)
|
||||
.collectionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if all the suggestion are configured with the same fixed collection
|
||||
* in the configuration.
|
||||
* @param suggestions
|
||||
*/
|
||||
public isCollectionFixed(suggestions: Suggestion[]): boolean {
|
||||
return this.getFixedCollectionIds(suggestions).length === 1;
|
||||
}
|
||||
|
||||
private getFixedCollectionIds(suggestions: Suggestion[]): string[] {
|
||||
const collectionIds = {};
|
||||
suggestions.forEach((suggestion: Suggestion) => {
|
||||
const conf = environment.suggestion.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source);
|
||||
if (hasValue(conf)) {
|
||||
collectionIds[conf.collectionId] = true;
|
||||
}
|
||||
});
|
||||
return Object.keys(collectionIds);
|
||||
}
|
||||
}
|
@@ -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 './reciter-suggestions/suggestion-targets/suggestion-targets.effects';
|
||||
|
||||
export const suggestionNotificationsEffects = [
|
||||
QualityAssuranceTopicsEffects,
|
||||
QualityAssuranceSourceEffects
|
||||
QualityAssuranceSourceEffects,
|
||||
SuggestionTargetsEffects
|
||||
];
|
||||
|
@@ -26,6 +26,20 @@ import { QualityAssuranceSourceService } from './qa/source/quality-assurance-sou
|
||||
import {
|
||||
QualityAssuranceSourceDataService
|
||||
} from '../core/suggestion-notifications/qa/source/quality-assurance-source-data.service';
|
||||
import { SuggestionTargetsComponent } from './reciter-suggestions/suggestion-targets/suggestion-targets.component';
|
||||
import { SuggestionActionsComponent } from './reciter-suggestions/suggestion-actions/suggestion-actions.component';
|
||||
import {
|
||||
SuggestionListElementComponent
|
||||
} from './reciter-suggestions/suggestion-list-element/suggestion-list-element.component';
|
||||
import {
|
||||
SuggestionEvidencesComponent
|
||||
} from './reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component';
|
||||
import { SuggestionsPopupComponent } from './reciter-suggestions/suggestions-popup/suggestions-popup.component';
|
||||
import {
|
||||
SuggestionsNotificationComponent
|
||||
} from './reciter-suggestions/suggestions-notification/suggestions-notification.component';
|
||||
import { SuggestionsService } from './reciter-suggestions/suggestions.service';
|
||||
import { SuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service';
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -40,7 +54,13 @@ const MODULES = [
|
||||
const COMPONENTS = [
|
||||
QualityAssuranceTopicsComponent,
|
||||
QualityAssuranceEventsComponent,
|
||||
QualityAssuranceSourceComponent
|
||||
QualityAssuranceSourceComponent,
|
||||
SuggestionTargetsComponent,
|
||||
SuggestionActionsComponent,
|
||||
SuggestionListElementComponent,
|
||||
SuggestionEvidencesComponent,
|
||||
SuggestionsPopupComponent,
|
||||
SuggestionsNotificationComponent
|
||||
];
|
||||
|
||||
const DIRECTIVES = [ ];
|
||||
@@ -55,7 +75,9 @@ const PROVIDERS = [
|
||||
QualityAssuranceSourceService,
|
||||
QualityAssuranceTopicDataService,
|
||||
QualityAssuranceSourceDataService,
|
||||
QualityAssuranceEventDataService
|
||||
QualityAssuranceEventDataService,
|
||||
SuggestionsService,
|
||||
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 './reciter-suggestions/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');
|
||||
|
11
src/app/suggestions-page/suggestions-page-routing-paths.ts
Normal file
11
src/app/suggestions-page/suggestions-page-routing-paths.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
|
||||
export const SUGGESTION_MODULE_PATH = 'suggestions';
|
||||
|
||||
export function getSuggestionModuleRoute() {
|
||||
return `/${SUGGESTION_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export function getSuggestionPageRoute(SuggestionId: string) {
|
||||
return new URLCombiner(getSuggestionModuleRoute(), SuggestionId).toString();
|
||||
}
|
36
src/app/suggestions-page/suggestions-page-routing.module.ts
Normal file
36
src/app/suggestions-page/suggestions-page-routing.module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { SuggestionsPageResolver } from './suggestions-page.resolver';
|
||||
import { SuggestionsPageComponent } from './suggestions-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: ':targetId',
|
||||
resolve: {
|
||||
suggestionTargets: SuggestionsPageResolver,
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: {
|
||||
title: 'admin.notifications.recitersuggestion.page.title',
|
||||
breadcrumbKey: 'admin.notifications.recitersuggestion',
|
||||
showBreadcrumbsFluid: false
|
||||
},
|
||||
canActivate: [AuthenticatedGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
component: SuggestionsPageComponent,
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
SuggestionsPageResolver
|
||||
]
|
||||
})
|
||||
export class SuggestionsPageRoutingModule {
|
||||
|
||||
}
|
48
src/app/suggestions-page/suggestions-page.component.html
Normal file
48
src/app/suggestions-page/suggestions-page.component.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-container *ngVar="(suggestionsRD$ | async) as suggestionsRD">
|
||||
<div *ngIf="suggestionsRD?.pageInfo?.totalElements > 0">
|
||||
|
||||
<h2>
|
||||
{{ translateSuggestionType() | translate }}
|
||||
{{'reciter.suggestion.suggestionFor' | translate}}
|
||||
<a target="_blank" [routerLink]="['/items', researcherUuid]">{{researcherName}}</a>
|
||||
{{'reciter.suggestion.from.source' | translate}} {{ translateSuggestionSource() | translate }}
|
||||
</h2>
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<button class="btn btn-light" (click)="onToggleSelectAll(suggestionsRD.page)">Select / Deselect All</button>
|
||||
<em>({{ getSelectedSuggestionsCount() }})</em>
|
||||
<ds-suggestion-actions *ngIf="getSelectedSuggestionsCount() > 0"
|
||||
class="mt-2 ml-2"
|
||||
[isBulk]="true"
|
||||
[isCollectionFixed]="isCollectionFixed(suggestionsRD.page)"
|
||||
(approveAndImport)="approveAndImportAllSelected($event)"
|
||||
(notMineClicked)="notMineAllSelected()"></ds-suggestion-actions>
|
||||
<i class='fas fa-circle-notch fa-spin' *ngIf="isBulkOperationPending"></i>
|
||||
</div>
|
||||
<ds-loading *ngIf="(processing$ | async)"></ds-loading>
|
||||
<ds-pagination *ngIf="!(processing$ | async)"
|
||||
[paginationOptions]="paginationOptions"
|
||||
[sortOptions]="paginationSortConfig"
|
||||
[collectionSize]="suggestionsRD?.pageInfo?.totalElements" [hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="false" [hidePaginationDetail]="false"
|
||||
(paginationChange)="onPaginationChange()">
|
||||
<ul class="list-unstyled">
|
||||
<li *ngFor="let object of suggestionsRD?.page; let i = index; let last = last" class="mt-4 mb-4">
|
||||
<ds-suggestion-list-item
|
||||
[object]="object"
|
||||
[isSelected]="selectedSuggestions[object.id]"
|
||||
[isCollectionFixed]="isCollectionFixed([object])"
|
||||
(notMineClicked)="notMine($event)"
|
||||
(selected)="onSelected(object, $event)"
|
||||
(approveAndImport)="approveAndImport($event)"></ds-suggestion-list-item>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
107
src/app/suggestions-page/suggestions-page.component.spec.ts
Normal file
107
src/app/suggestions-page/suggestions-page.component.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { SuggestionsPageComponent } from './suggestions-page.component';
|
||||
import { SuggestionListElementComponent } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component';
|
||||
import { SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service';
|
||||
import { getMockSuggestionNotificationsStateService, getMockSuggestionsService } from '../shared/mocks/suggestion.mock';
|
||||
import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model';
|
||||
import { Suggestion } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
import { mockSuggestionPublicationOne, mockSuggestionPublicationTwo } from '../shared/mocks/reciter-suggestion.mock';
|
||||
import { SuggestionEvidencesComponent } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-evidences/suggestion-evidences.component';
|
||||
import { ObjectKeysPipe } from '../shared/utils/object-keys-pipe';
|
||||
import { VarDirective } from '../shared/utils/var.directive';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../shared/testing/router.stub';
|
||||
import { mockSuggestionTargetsObjectOne } from '../shared/mocks/reciter-suggestion-targets.mock';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||
import { getMockTranslateService } from '../shared/mocks/translate.service.mock';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service';
|
||||
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../shared/remote-data.utils';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
|
||||
describe('SuggestionPageComponent', () => {
|
||||
let component: SuggestionsPageComponent;
|
||||
let fixture: ComponentFixture<SuggestionsPageComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
const mockSuggestionsService = getMockSuggestionsService();
|
||||
const mockSuggestionsTargetStateService = getMockSuggestionNotificationsStateService();
|
||||
const suggestionTargetsList: PaginatedList<Suggestion> = buildPaginatedList(new PageInfo(), [mockSuggestionPublicationOne, mockSuggestionPublicationTwo]);
|
||||
const router = new RouterStub();
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
suggestionTargets: createSuccessfulRemoteDataObject(mockSuggestionTargetsObjectOne)
|
||||
}),
|
||||
queryParams: observableOf({})
|
||||
};
|
||||
const workspaceitemServiceMock = jasmine.createSpyObj('WorkspaceitemDataService', {
|
||||
importExternalSourceEntry: jasmine.createSpy('importExternalSourceEntry')
|
||||
});
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
SuggestionEvidencesComponent,
|
||||
SuggestionListElementComponent,
|
||||
SuggestionsPageComponent,
|
||||
ObjectKeysPipe,
|
||||
VarDirective
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: WorkspaceitemDataService, useValue: workspaceitemServiceMock },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: SuggestionsService, useValue: mockSuggestionsService },
|
||||
{ provide: SuggestionTargetsStateService, useValue: mockSuggestionsTargetStateService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
SuggestionsPageComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents().then();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SuggestionsPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
spyOn(component, 'updatePage').and.stub();
|
||||
|
||||
scheduler.schedule(() => fixture.detectChanges());
|
||||
scheduler.flush();
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.suggestionId).toBe(mockSuggestionTargetsObjectOne.id);
|
||||
expect(component.researcherName).toBe(mockSuggestionTargetsObjectOne.display);
|
||||
expect(component.updatePage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
286
src/app/suggestions-page/suggestions-page.component.ts
Normal file
286
src/app/suggestions-page/suggestions-page.component.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Data, Router } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { SortDirection, SortOptions, } from '../core/cache/models/sort-options.model';
|
||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import { SuggestionBulkResult, SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { Suggestion } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion.model';
|
||||
import { SuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { SuggestionApproveAndImport } from '../suggestion-notifications/reciter-suggestions/suggestion-list-element/suggestion-list-element.component';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { SuggestionTargetsStateService } from '../suggestion-notifications/reciter-suggestions/suggestion-targets/suggestion-targets.state.service';
|
||||
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
|
||||
import {FindListOptions} from '../core/data/find-list-options.model';
|
||||
import {redirectOn4xx} from '../core/shared/authorized.operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-suggestion-page',
|
||||
templateUrl: './suggestions-page.component.html',
|
||||
styleUrls: ['./suggestions-page.component.scss'],
|
||||
})
|
||||
export class SuggestionsPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The pagination configuration
|
||||
*/
|
||||
paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'sp',
|
||||
pageSizeOptions: [5, 10, 20, 40, 60]
|
||||
});
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
*/
|
||||
paginationSortConfig: SortOptions = new SortOptions('trust', SortDirection.DESC);
|
||||
|
||||
/**
|
||||
* The FindListOptions object
|
||||
*/
|
||||
defaultConfig: FindListOptions = Object.assign(new FindListOptions(), {sort: this.paginationSortConfig});
|
||||
|
||||
/**
|
||||
* A boolean representing if results are loading
|
||||
*/
|
||||
public processing$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A list of remote data objects of suggestions
|
||||
*/
|
||||
suggestionsRD$: BehaviorSubject<PaginatedList<Suggestion>> = new BehaviorSubject<PaginatedList<Suggestion>>({} as any);
|
||||
|
||||
targetRD$: Observable<RemoteData<SuggestionTarget>>;
|
||||
targetId$: Observable<string>;
|
||||
|
||||
suggestionTarget: SuggestionTarget;
|
||||
suggestionId: any;
|
||||
suggestionSource: any;
|
||||
researcherName: any;
|
||||
researcherUuid: any;
|
||||
|
||||
selectedSuggestions: { [id: string]: Suggestion } = {};
|
||||
isBulkOperationPending = false;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private notificationService: NotificationsService,
|
||||
private paginationService: PaginationService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private suggestionService: SuggestionsService,
|
||||
private suggestionTargetsStateService: SuggestionTargetsStateService,
|
||||
private translateService: TranslateService,
|
||||
private workspaceItemService: WorkspaceitemDataService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.targetRD$ = this.route.data.pipe(
|
||||
map((data: Data) => data.suggestionTargets as RemoteData<SuggestionTarget>),
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
);
|
||||
|
||||
this.targetId$ = this.targetRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((target: SuggestionTarget) => target.id)
|
||||
);
|
||||
this.targetRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((suggestionTarget: SuggestionTarget) => {
|
||||
this.suggestionTarget = suggestionTarget;
|
||||
this.suggestionId = suggestionTarget.id;
|
||||
this.researcherName = suggestionTarget.display;
|
||||
this.suggestionSource = suggestionTarget.source;
|
||||
this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget);
|
||||
this.updatePage();
|
||||
});
|
||||
|
||||
this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
*/
|
||||
onPaginationChange() {
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of suggestions
|
||||
*/
|
||||
updatePage() {
|
||||
this.processing$.next(true);
|
||||
const pageConfig$: Observable<FindListOptions> = this.paginationService.getFindListOptions(
|
||||
this.paginationOptions.id,
|
||||
this.defaultConfig,
|
||||
).pipe(
|
||||
distinctUntilChanged()
|
||||
);
|
||||
combineLatest([this.targetId$, pageConfig$]).pipe(
|
||||
switchMap(([targetId, config]: [string, FindListOptions]) => {
|
||||
return this.suggestionService.getSuggestions(
|
||||
targetId,
|
||||
config.elementsPerPage,
|
||||
config.currentPage,
|
||||
config.sort
|
||||
);
|
||||
}),
|
||||
take(1)
|
||||
).subscribe((results: PaginatedList<Suggestion>) => {
|
||||
this.processing$.next(false);
|
||||
this.suggestionsRD$.next(results);
|
||||
this.suggestionService.clearSuggestionRequests();
|
||||
// navigate to the mydspace if no suggestions remains
|
||||
|
||||
// if (results.totalElements === 0) {
|
||||
// const content = this.translateService.instant('reciter.suggestion.empty',
|
||||
// this.suggestionService.getNotificationSuggestionInterpolation(this.suggestionTarget));
|
||||
// this.notificationService.success('', content, {timeOut:0}, true);
|
||||
// TODO if the target is not the current use route to the suggestion target page
|
||||
// this.router.navigate(['/mydspace']);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete a suggestion.
|
||||
* @suggestionId
|
||||
*/
|
||||
notMine(suggestionId) {
|
||||
this.suggestionService.notMine(suggestionId).subscribe((res) => {
|
||||
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
|
||||
this.updatePage();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete all selected suggestions.
|
||||
*/
|
||||
notMineAllSelected() {
|
||||
this.isBulkOperationPending = true;
|
||||
this.suggestionService
|
||||
.notMineMultiple(Object.values(this.selectedSuggestions))
|
||||
.subscribe((results: SuggestionBulkResult) => {
|
||||
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
|
||||
this.updatePage();
|
||||
this.isBulkOperationPending = false;
|
||||
this.selectedSuggestions = {};
|
||||
if (results.success > 0) {
|
||||
this.notificationService.success(
|
||||
this.translateService.get('reciter.suggestion.notMine.bulk.success',
|
||||
{count: results.success}));
|
||||
}
|
||||
if (results.fails > 0) {
|
||||
this.notificationService.error(
|
||||
this.translateService.get('reciter.suggestion.notMine.bulk.error',
|
||||
{count: results.fails}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to approve & import.
|
||||
* @param event contains the suggestion and the target collection
|
||||
*/
|
||||
approveAndImport(event: SuggestionApproveAndImport) {
|
||||
this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId)
|
||||
.subscribe((workspaceitem: WorkspaceItem) => {
|
||||
const content = this.translateService.instant('reciter.suggestion.approveAndImport.success', { workspaceItemId: workspaceitem.id });
|
||||
this.notificationService.success('', content, {timeOut:0}, true);
|
||||
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
|
||||
this.updatePage();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to approve & import all selected suggestions.
|
||||
* @param event contains the target collection
|
||||
*/
|
||||
approveAndImportAllSelected(event: SuggestionApproveAndImport) {
|
||||
this.isBulkOperationPending = true;
|
||||
this.suggestionService
|
||||
.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId)
|
||||
.subscribe((results: SuggestionBulkResult) => {
|
||||
this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction();
|
||||
this.updatePage();
|
||||
this.isBulkOperationPending = false;
|
||||
this.selectedSuggestions = {};
|
||||
if (results.success > 0) {
|
||||
this.notificationService.success(
|
||||
this.translateService.get('reciter.suggestion.approveAndImport.bulk.success',
|
||||
{count: results.success}));
|
||||
}
|
||||
if (results.fails > 0) {
|
||||
this.notificationService.error(
|
||||
this.translateService.get('reciter.suggestion.approveAndImport.bulk.error',
|
||||
{count: results.fails}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When a specific suggestion is selected.
|
||||
* @param object the suggestions
|
||||
* @param selected the new selected value for the suggestion
|
||||
*/
|
||||
onSelected(object: Suggestion, selected: boolean) {
|
||||
if (selected) {
|
||||
this.selectedSuggestions[object.id] = object;
|
||||
} else {
|
||||
delete this.selectedSuggestions[object.id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When Toggle Select All occurs.
|
||||
* @param suggestions all the visible suggestions inside the page
|
||||
*/
|
||||
onToggleSelectAll(suggestions: Suggestion[]) {
|
||||
if ( this.getSelectedSuggestionsCount() > 0) {
|
||||
this.selectedSuggestions = {};
|
||||
} else {
|
||||
suggestions.forEach((suggestion) => {
|
||||
this.selectedSuggestions[suggestion.id] = suggestion;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of selected suggestions.
|
||||
*/
|
||||
getSelectedSuggestionsCount(): number {
|
||||
return Object.keys(this.selectedSuggestions).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if all the suggestion are configured with the same fixed collection in the configuration.
|
||||
* @param suggestions
|
||||
*/
|
||||
isCollectionFixed(suggestions: Suggestion[]): boolean {
|
||||
return this.suggestionService.isCollectionFixed(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Label to be used to translate the suggestion source.
|
||||
*/
|
||||
translateSuggestionSource() {
|
||||
return this.suggestionService.translateSuggestionSource(this.suggestionSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Label to be used to translate the suggestion type.
|
||||
*/
|
||||
translateSuggestionType() {
|
||||
return this.suggestionService.translateSuggestionType(this.suggestionSource);
|
||||
}
|
||||
|
||||
}
|
24
src/app/suggestions-page/suggestions-page.module.ts
Normal file
24
src/app/suggestions-page/suggestions-page.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { SuggestionsPageComponent } from './suggestions-page.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SuggestionsPageRoutingModule } from './suggestions-page-routing.module';
|
||||
import { SuggestionsService } from '../suggestion-notifications/reciter-suggestions/suggestions.service';
|
||||
import { SuggestionNotificationsModule } from '../suggestion-notifications/suggestion-notifications.module';
|
||||
import { SuggestionsDataService } from '../core/suggestion-notifications/reciter-suggestions/suggestions-data.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SuggestionsPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SuggestionNotificationsModule,
|
||||
SuggestionsPageRoutingModule
|
||||
],
|
||||
providers: [
|
||||
SuggestionsDataService,
|
||||
SuggestionsService
|
||||
]
|
||||
})
|
||||
export class SuggestionsPageModule { }
|
34
src/app/suggestions-page/suggestions-page.resolver.ts
Normal file
34
src/app/suggestions-page/suggestions-page.resolver.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { find } from 'rxjs/operators';
|
||||
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { SuggestionTarget } from '../core/suggestion-notifications/reciter-suggestions/models/suggestion-target.model';
|
||||
import {
|
||||
SuggestionTargetDataService
|
||||
} from '../core/suggestion-notifications/reciter-suggestions/target/suggestion-target-data.service';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific collection before the route is activated
|
||||
*/
|
||||
@Injectable()
|
||||
export class SuggestionsPageResolver implements Resolve<RemoteData<SuggestionTarget>> {
|
||||
constructor(private suggestionsDataService: SuggestionTargetDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a suggestion target based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
|
||||
* or an error if something went wrong
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<SuggestionTarget>> {
|
||||
return this.suggestionsDataService.getTargetById(route.params.targetId).pipe(
|
||||
find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded),
|
||||
);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ import { AuthConfig } from './auth-config.interfaces';
|
||||
import { UIServerConfig } from './ui-server-config.interface';
|
||||
import { MediaViewerConfig } from './media-viewer-config.interface';
|
||||
import { BrowseByConfig } from './browse-by-config.interface';
|
||||
import {SuggestionConfig} from './layout-config.interfaces';
|
||||
import { BundleConfig } from './bundle-config.interface';
|
||||
import { ActuatorsConfig } from './actuators.config';
|
||||
import { InfoConfig } from './info-config.interface';
|
||||
@@ -42,6 +43,7 @@ interface AppConfig extends Config {
|
||||
collection: CollectionPageConfig;
|
||||
themes: ThemeConfig[];
|
||||
mediaViewer: MediaViewerConfig;
|
||||
suggestion: SuggestionConfig[];
|
||||
bundle: BundleConfig;
|
||||
actuators: ActuatorsConfig
|
||||
info: InfoConfig;
|
||||
|
@@ -14,6 +14,7 @@ import { ServerConfig } from './server-config.interface';
|
||||
import { SubmissionConfig } from './submission-config.interface';
|
||||
import { ThemeConfig } from './theme.model';
|
||||
import { UIServerConfig } from './ui-server-config.interface';
|
||||
import {SuggestionConfig} from './layout-config.interfaces';
|
||||
import { BundleConfig } from './bundle-config.interface';
|
||||
import { ActuatorsConfig } from './actuators.config';
|
||||
import { InfoConfig } from './info-config.interface';
|
||||
@@ -294,6 +295,14 @@ export class DefaultAppConfig implements AppConfig {
|
||||
}
|
||||
};
|
||||
|
||||
suggestion: SuggestionConfig[] = [
|
||||
// {
|
||||
// // Use this configuration to map a suggestion import to a specific collection based on the suggestion type.
|
||||
// source: 'suggestionSource',
|
||||
// collectionId: 'collectionUUID'
|
||||
// }
|
||||
];
|
||||
|
||||
// Theme Config
|
||||
themes: ThemeConfig[] = [
|
||||
// Add additional themes here. In the case where multiple themes match a route, the first one
|
||||
|
46
src/config/layout-config.interfaces.ts
Normal file
46
src/config/layout-config.interfaces.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Config } from './config.interface';
|
||||
|
||||
export interface UrnConfig extends Config {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface CrisRefConfig extends Config {
|
||||
entityType: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface CrisLayoutMetadataBoxConfig extends Config {
|
||||
defaultMetadataLabelColStyle: string;
|
||||
defaultMetadataValueColStyle: string;
|
||||
}
|
||||
|
||||
export interface CrisLayoutTypeConfig {
|
||||
orientation: string;
|
||||
}
|
||||
|
||||
export interface NavbarConfig extends Config {
|
||||
showCommunityCollection: boolean;
|
||||
}
|
||||
|
||||
export interface CrisItemPageConfig extends Config {
|
||||
[entity: string]: CrisLayoutTypeConfig;
|
||||
default: CrisLayoutTypeConfig;
|
||||
}
|
||||
|
||||
|
||||
export interface CrisLayoutConfig extends Config {
|
||||
urn: UrnConfig[];
|
||||
crisRef: CrisRefConfig[];
|
||||
itemPage: CrisItemPageConfig;
|
||||
metadataBox: CrisLayoutMetadataBoxConfig;
|
||||
}
|
||||
|
||||
export interface LayoutConfig extends Config {
|
||||
navbar: NavbarConfig;
|
||||
}
|
||||
|
||||
export interface SuggestionConfig extends Config {
|
||||
source: string;
|
||||
collectionId: string;
|
||||
}
|
Reference in New Issue
Block a user