mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into reorder-name-variants
This commit is contained in:
@@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
return browser.get('/')
|
||||
.then(() => browser.waitForAngular());
|
||||
}
|
||||
|
||||
getPageTitleText() {
|
||||
|
@@ -3,6 +3,7 @@
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
|
@@ -11,12 +11,14 @@ import { EditCollectionPageComponent } from './edit-collection-page/edit-collect
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CollectionPageRoutingModule
|
||||
CollectionPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
|
||||
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<!-- Community logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
||||
|
@@ -6,17 +6,19 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CommunityPageRoutingModule
|
||||
CommunityPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
|
@@ -2,12 +2,25 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { HomePageResolver } from './home-page.resolver';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } }
|
||||
{
|
||||
path: '',
|
||||
component: HomePageComponent,
|
||||
pathMatch: 'full',
|
||||
data: {title: 'home.title'},
|
||||
resolve: {
|
||||
site: HomePageResolver
|
||||
}
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
HomePageResolver
|
||||
]
|
||||
})
|
||||
export class HomePageRoutingModule { }
|
||||
export class HomePageRoutingModule {
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
<ds-home-news></ds-home-news>
|
||||
<div class="container">
|
||||
<ng-container *ngIf="(site$ | async) as site">
|
||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||
</ng-container>
|
||||
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
||||
|
@@ -1,9 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-home-page',
|
||||
styleUrls: ['./home-page.component.scss'],
|
||||
templateUrl: './home-page.component.html'
|
||||
})
|
||||
export class HomePageComponent {
|
||||
export class HomePageComponent implements OnInit {
|
||||
|
||||
site$:Observable<Site>;
|
||||
|
||||
constructor(
|
||||
private route:ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.site$ = this.route.data.pipe(
|
||||
map((data) => data.site as Site),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,14 @@ import { HomePageRoutingModule } from './home-page-routing.module';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
HomePageRoutingModule
|
||||
HomePageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
HomePageComponent,
|
||||
|
25
src/app/+home-page/home-page.resolver.ts
Normal file
25
src/app/+home-page/home-page.resolver.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { SiteDataService } from '../core/data/site-data.service';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* The class that resolve the Site object for a route
|
||||
*/
|
||||
@Injectable()
|
||||
export class HomePageResolver implements Resolve<Site> {
|
||||
constructor(private siteService:SiteDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a site object
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
|
||||
*/
|
||||
resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<Site> | Promise<Site> | Site {
|
||||
return this.siteService.find().pipe(take(1));
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||
<div class="simple-view-link my-3">
|
||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
|
||||
|
@@ -26,6 +26,7 @@ import { MetadataRepresentationListComponent } from './simple/metadata-represent
|
||||
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
||||
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -33,7 +34,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
||||
SharedModule,
|
||||
ItemPageRoutingModule,
|
||||
EditItemPageModule,
|
||||
SearchPageModule
|
||||
SearchPageModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
ItemPageComponent,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1 +1 @@
|
||||
@import '../+search-page/search-page.component.scss';
|
||||
@import '../+search-page/search.component.scss';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
@@ -15,8 +15,8 @@ import { SearchService } from '../core/shared/search/search.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-configuration-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
@@ -27,7 +27,7 @@ import { SearchService } from '../core/shared/search/search.service';
|
||||
]
|
||||
})
|
||||
|
||||
export class ConfigurationSearchPageComponent extends SearchPageComponent implements OnInit {
|
||||
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
|
||||
/**
|
||||
* The configuration to use for the search options
|
||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
|
||||
describe('FilteredSearchPageComponent', () => {
|
||||
let comp: FilteredSearchPageComponent;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
@@ -17,8 +17,8 @@ import { RouteService } from '../core/services/route.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-filtered-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
@@ -29,7 +29,7 @@ import { RouteService } from '../core/services/route.service';
|
||||
]
|
||||
})
|
||||
|
||||
export class FilteredSearchPageComponent extends SearchPageComponent implements OnInit {
|
||||
export class FilteredSearchPageComponent extends SearchComponent implements OnInit {
|
||||
/**
|
||||
* The actual query for the fixed filter.
|
||||
* If empty, the query will be determined by the route parameter called 'fixedFilterQuery'
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@@ -1,54 +1,2 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<div class="row mb-3 mb-md-1">
|
||||
<div class="labels col-sm-9 offset-sm-3">
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ds-search></ds-search>
|
||||
<ds-search-tracker></ds-search-tracker>
|
||||
|
@@ -1,187 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
import { SearchResult } from '../shared/search/search-result.model';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
import { currentPath } from '../shared/utils/route.utils';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
export const SEARCH_ROUTE = '/search';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
* The route parameter 'id' is used to request the item it represents.
|
||||
* All fields of the item that should be displayed, are defined in its template.
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component represents the whole search page
|
||||
* It renders search results depending on the current search options
|
||||
*/
|
||||
export class SearchPageComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService,
|
||||
protected router: Router) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening to changes in the paginated search options
|
||||
* If something changes, update the search results
|
||||
*
|
||||
* Listen to changes in the scope
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return currentPath(this.router);
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
export class SearchPageComponent {
|
||||
}
|
||||
|
@@ -7,11 +7,18 @@ import { SearchPageComponent } from './search-page.component';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchTrackerComponent } from './search-tracker.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
SearchComponent,
|
||||
FilteredSearchPageComponent,
|
||||
ConfigurationSearchPageComponent
|
||||
ConfigurationSearchPageComponent,
|
||||
SearchTrackerComponent,
|
||||
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -19,7 +26,8 @@ const components = [
|
||||
SearchPageRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CoreModule.forRoot()
|
||||
CoreModule.forRoot(),
|
||||
StatisticsModule.forRoot(),
|
||||
],
|
||||
providers: [ConfigurationSearchPageGuard],
|
||||
declarations: components,
|
||||
|
1
src/app/+search-page/search-tracker.component.html
Normal file
1
src/app/+search-page/search-tracker.component.html
Normal file
@@ -0,0 +1 @@
|
||||
|
3
src/app/+search-page/search-tracker.component.scss
Normal file
3
src/app/+search-page/search-tracker.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: none
|
||||
}
|
88
src/app/+search-page/search-tracker.component.ts
Normal file
88
src/app/+search-page/search-tracker.component.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { SearchQueryResponse } from './search-service/search-query-response.model';
|
||||
import { SearchSuccessResponse } from '../core/cache/response.models';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
|
||||
/**
|
||||
* This component triggers a page view statistic
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-search-tracker',
|
||||
styleUrls: ['./search-tracker.component.scss'],
|
||||
templateUrl: './search-tracker.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
protected service:SearchService,
|
||||
protected sidebarService:SidebarService,
|
||||
protected windowService:HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService,
|
||||
protected routeService:RouteService,
|
||||
public angulartics2:Angulartics2
|
||||
) {
|
||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
// super.ngOnInit();
|
||||
this.getSearchOptions().pipe(
|
||||
switchMap((options) => this.service.searchEntries(options)
|
||||
.pipe(
|
||||
filter((entry) =>
|
||||
hasValue(entry.requestEntry)
|
||||
&& hasValue(entry.requestEntry.response)
|
||||
&& entry.requestEntry.response.isSuccessful === true
|
||||
),
|
||||
map((entry) => ({
|
||||
searchOptions: entry.searchOptions,
|
||||
response: (entry.requestEntry.response as SearchSuccessResponse).results
|
||||
})),
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe((entry) => {
|
||||
const config:PaginatedSearchOptions = entry.searchOptions;
|
||||
const searchQueryResponse:SearchQueryResponse = entry.response;
|
||||
const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = [];
|
||||
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||
const appliedFilter = appliedFilters[i];
|
||||
filters.push(appliedFilter);
|
||||
}
|
||||
this.angulartics2.eventTrack.next({
|
||||
action: 'search',
|
||||
properties: {
|
||||
searchOptions: config,
|
||||
page: {
|
||||
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
|
||||
totalElements: searchQueryResponse.page.totalElements,
|
||||
totalPages: searchQueryResponse.page.totalPages,
|
||||
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
|
||||
},
|
||||
sort: {
|
||||
by: config.sort.field,
|
||||
order: config.sort.direction
|
||||
},
|
||||
filters: filters,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
50
src/app/+search-page/search.component.html
Normal file
50
src/app/+search-page/search.component.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</ng-template>
|
@@ -10,7 +10,7 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
@@ -25,11 +25,11 @@ import { SearchConfigurationServiceStub } from '../shared/testing/search-configu
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
|
||||
let comp: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
let comp: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||
const store: Store<SearchComponent> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
@@ -143,14 +143,14 @@ export function configureSearchComponentTestingModule(compType) {
|
||||
}).compileComponents();
|
||||
}
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
describe('SearchComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
configureSearchComponentTestingModule(SearchPageComponent);
|
||||
configureSearchComponentTestingModule(SearchComponent);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchPageComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
comp = fixture.componentInstance; // SearchComponent test instance
|
||||
comp.inPlaceSearch = false;
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
175
src/app/+search-page/search.component.ts
Normal file
175
src/app/+search-page/search.component.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component renders a sidebar, a search input bar and the search results.
|
||||
*/
|
||||
export class SearchComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening to changes in the paginated search options
|
||||
* If something changes, update the search results
|
||||
*
|
||||
* Listen to changes in the scope
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return './';
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -46,6 +46,7 @@ import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
||||
import { MockRouter } from './shared/mocks/mock-router';
|
||||
import { MockCookieService } from './shared/mocks/mock-cookie.service';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@@ -74,6 +75,7 @@ describe('App component', () => {
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
|
@@ -25,6 +25,7 @@ import { HostWindowService } from './shared/host-window.service';
|
||||
import { Theme } from '../config/theme.inferface';
|
||||
import { isNotEmpty } from './shared/empty.util';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
export const LANG_COOKIE = 'language_cookie';
|
||||
|
||||
@@ -51,6 +52,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private store: Store<HostWindowState>,
|
||||
private metadata: MetadataService,
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private angulartics2DSpace: Angulartics2DSpace,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private cssService: CSSVariableService,
|
||||
@@ -80,6 +82,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
angulartics2DSpace.startTracking();
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
|
||||
if (config.debug) {
|
||||
|
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inheritSerialization } from 'cerialize';
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { Site } from '../../shared/site.model';
|
||||
|
||||
/**
|
||||
* Normalized model class for a Site object
|
||||
*/
|
||||
@mapsTo(Site)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedSite extends NormalizedDSpaceObject<Site> {
|
||||
|
||||
}
|
@@ -121,6 +121,8 @@ import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
import { SiteDataService } from './data/site-data.service';
|
||||
import { NormalizedSite } from './cache/models/normalized-site.model';
|
||||
|
||||
import {
|
||||
MOCK_RESPONSE_MAP,
|
||||
@@ -160,6 +162,7 @@ const PROVIDERS = [
|
||||
AuthResponseParsingService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
SiteDataService,
|
||||
DSOResponseParsingService,
|
||||
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
|
||||
{ provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [GLOBAL_CONFIG, MOCK_RESPONSE_MAP, HttpClient]},
|
||||
@@ -261,6 +264,7 @@ export const normalizedModels =
|
||||
NormalizedBitstream,
|
||||
NormalizedBitstreamFormat,
|
||||
NormalizedItem,
|
||||
NormalizedSite,
|
||||
NormalizedCollection,
|
||||
NormalizedCommunity,
|
||||
NormalizedEPerson,
|
||||
|
@@ -22,6 +22,10 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
||||
}
|
||||
};
|
||||
const payload = data.payload._embedded.searchResult || emptyPayload;
|
||||
payload.appliedFilters = data.payload.appliedFilters;
|
||||
payload.sort = data.payload.sort;
|
||||
payload.scope = data.payload.scope;
|
||||
payload.configuration = data.payload.configuration;
|
||||
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||
.map((object) => object.hitHighlights)
|
||||
.map((hhObject) => {
|
||||
|
104
src/app/core/data/site-data.service.spec.ts
Normal file
104
src/app/core/data/site-data.service.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { SiteDataService } from './site-data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RemoteData } from './remote-data';
|
||||
|
||||
describe('SiteDataService', () => {
|
||||
let scheduler:TestScheduler;
|
||||
let service:SiteDataService;
|
||||
let halService:HALEndpointService;
|
||||
let requestService:RequestService;
|
||||
let rdbService:RemoteDataBuildService;
|
||||
let objectCache:ObjectCacheService;
|
||||
|
||||
const testObject = Object.assign(new Site(), {
|
||||
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
|
||||
});
|
||||
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const options = Object.assign(new FindAllOptions(), {});
|
||||
|
||||
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
|
||||
return observableOf({
|
||||
response: new RestResponse(successful, statusCode, statusText)
|
||||
} as RequestEntry);
|
||||
};
|
||||
|
||||
const siteLink = 'https://rest.api/rest/api/config/sites';
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', {a: siteLink})
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true,
|
||||
getByHref: getRequestEntry$(true, 200, 'Success')
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
})
|
||||
});
|
||||
|
||||
const store = {} as Store<CoreState>;
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||
|
||||
service = new SiteDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
dataBuildService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
});
|
||||
|
||||
describe('getBrowseEndpoint', () => {
|
||||
it('should return the Static Page endpoint', () => {
|
||||
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('b', {b: siteLink});
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('should return the Site object', () => {
|
||||
|
||||
spyOn(service, 'findAll').and.returnValue(cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
}));
|
||||
|
||||
const expected = cold('(b|)', {b: testObject});
|
||||
const result = service.find();
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
68
src/app/core/data/site-data.service.ts
Normal file
68
src/app/core/data/site-data.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { DataService } from './data.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
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 { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { getSucceededRemoteData } from '../shared/operators';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Site object
|
||||
*/
|
||||
@Injectable()
|
||||
export class SiteDataService extends DataService<Site> {
|
||||
|
||||
protected linkPath = 'sites';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
|
||||
constructor(
|
||||
protected requestService:RequestService,
|
||||
protected rdbService:RemoteDataBuildService,
|
||||
protected dataBuildService:NormalizedObjectBuildService,
|
||||
protected store:Store<CoreState>,
|
||||
protected objectCache:ObjectCacheService,
|
||||
protected halService:HALEndpointService,
|
||||
protected notificationsService:NotificationsService,
|
||||
protected http:HttpClient,
|
||||
protected comparator:DSOChangeAnalyzer<Site>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing the site object
|
||||
* @param {FindAllOptions} options
|
||||
* @param {Observable<string>} linkPath
|
||||
*/
|
||||
getBrowseEndpoint(options:FindAllOptions, linkPath?:string):Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the Site Object
|
||||
*/
|
||||
find():Observable<Site> {
|
||||
return this.findAll().pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((remoteData:RemoteData<PaginatedList<Site>>) => remoteData.payload),
|
||||
map((list:PaginatedList<Site>) => list.page[0])
|
||||
);
|
||||
}
|
||||
}
|
@@ -33,6 +33,7 @@ import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import { configureRequest, filterSuccessfulResponses, getResponseFromEntry, getSucceededRemoteData } from '../operators';
|
||||
import { RouteService } from '../../services/route.service';
|
||||
import { RequestEntry } from '../../data/request.reducer';
|
||||
|
||||
/**
|
||||
* Service that performs all general actions that have to do with the search page
|
||||
@@ -88,9 +89,9 @@ export class SearchService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
getEndpoint(searchOptions?:PaginatedSearchOptions):Observable<string> {
|
||||
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
if (hasValue(searchOptions)) {
|
||||
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||
} else {
|
||||
@@ -107,32 +108,60 @@ export class SearchService implements OnDestroy {
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
return this.getPaginatedResults(this.searchEntries(searchOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve request entries for search results from the server
|
||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
||||
*/
|
||||
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
||||
:Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
|
||||
|
||||
const hrefObs = this.getEndpoint(searchOptions);
|
||||
|
||||
const requestObs = hrefObs.pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||
|
||||
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||
const getResponseParserFn:() => GenericConstructor<ResponseParsingService> = () => {
|
||||
return this.parser;
|
||||
};
|
||||
|
||||
return Object.assign(request, {
|
||||
responseMsToLive: hasValue(responseMsToLive) ? responseMsToLive : request.responseMsToLive,
|
||||
getResponseParser: getResponseParserFn
|
||||
getResponseParser: getResponseParserFn,
|
||||
searchOptions: searchOptions
|
||||
});
|
||||
}),
|
||||
configureRequest(this.requestService),
|
||||
);
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
return requestObs.pipe(
|
||||
switchMap((request:RestRequest) => this.requestService.getByHref(request.href)),
|
||||
map(((requestEntry:RequestEntry) => ({
|
||||
searchOptions: searchOptions,
|
||||
requestEntry: requestEntry
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to convert the parsed responses into a paginated list of search results
|
||||
* @param searchEntries: The request entries from the search method
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>)
|
||||
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
const requestEntryObs:Observable<RequestEntry> = searchEntries.pipe(
|
||||
map((entry) => entry.requestEntry),
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
const sqrObs:Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((response: SearchSuccessResponse) => response.results)
|
||||
map((response:SearchSuccessResponse) => response.results),
|
||||
);
|
||||
|
||||
// turn dspace href from search results to effective list of DSpaceObjects
|
||||
@@ -178,11 +207,12 @@ export class SearchService implements OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
return observableCombineLatest(hrefObs, tDomainListObs, requestEntryObs).pipe(
|
||||
switchMap(([href, tDomainList, requestEntry]) => {
|
||||
return observableCombineLatest(tDomainListObs, searchEntries).pipe(
|
||||
switchMap(([tDomainList, searchEntry]) => {
|
||||
const requestEntry = searchEntry.requestEntry;
|
||||
if (tDomainList.indexOf(undefined) > -1 && requestEntry && requestEntry.completed) {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
return this.search(searchOptions)
|
||||
this.requestService.removeByHrefSubstring(requestEntry.request.href);
|
||||
return this.search(searchEntry.searchOptions)
|
||||
} else {
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
}
|
||||
|
11
src/app/core/shared/site.model.ts
Normal file
11
src/app/core/shared/site.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* Model class for the Site object
|
||||
*/
|
||||
export class Site extends DSpaceObject {
|
||||
|
||||
static type = new ResourceType('site');
|
||||
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
/* tslint:disable:no-empty */
|
||||
export class AngularticsMock {
|
||||
public eventTrack(action, properties) { }
|
||||
public startTracking():void {}
|
||||
}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import {of as observableOf, Observable } from 'rxjs';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
|
||||
export function getMockRequestService(requestEntry$: Observable<RequestEntry> = observableOf(new RequestEntry())): RequestService {
|
||||
export function getMockRequestService(requestEntry$: Observable<RequestEntry> = observableOf(new RequestEntry())): SpyObj<RequestService> {
|
||||
return jasmine.createSpyObj('requestService', {
|
||||
configure: false,
|
||||
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||
|
26
src/app/statistics/angulartics/dspace-provider.spec.ts
Normal file
26
src/app/statistics/angulartics/dspace-provider.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Angulartics2DSpace } from './dspace-provider';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { StatisticsService } from '../statistics.service';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('Angulartics2DSpace', () => {
|
||||
let provider:Angulartics2DSpace;
|
||||
let angulartics2:Angulartics2;
|
||||
let statisticsService:jasmine.SpyObj<StatisticsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
angulartics2 = {
|
||||
eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}),
|
||||
filterDeveloperMode: () => filter(() => true)
|
||||
} as any;
|
||||
statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null});
|
||||
provider = new Angulartics2DSpace(angulartics2, statisticsService);
|
||||
});
|
||||
|
||||
it('should use the statisticsService', () => {
|
||||
provider.startTracking();
|
||||
expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object');
|
||||
});
|
||||
|
||||
});
|
38
src/app/statistics/angulartics/dspace-provider.ts
Normal file
38
src/app/statistics/angulartics/dspace-provider.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { StatisticsService } from '../statistics.service';
|
||||
|
||||
/**
|
||||
* Angulartics2DSpace is a angulartics2 plugin that provides DSpace with the events.
|
||||
*/
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class Angulartics2DSpace {
|
||||
|
||||
constructor(
|
||||
private angulartics2:Angulartics2,
|
||||
private statisticsService:StatisticsService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates this plugin
|
||||
*/
|
||||
startTracking():void {
|
||||
this.angulartics2.eventTrack
|
||||
.pipe(this.angulartics2.filterDeveloperMode())
|
||||
.subscribe((event) => this.eventTrack(event));
|
||||
}
|
||||
|
||||
private eventTrack(event) {
|
||||
if (event.action === 'pageView') {
|
||||
this.statisticsService.trackViewEvent(event.properties.object);
|
||||
} else if (event.action === 'search') {
|
||||
this.statisticsService.trackSearchEvent(
|
||||
event.properties.searchOptions,
|
||||
event.properties.page,
|
||||
event.properties.sort,
|
||||
event.properties.filters
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: none
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
|
||||
/**
|
||||
* This component triggers a page view statistic
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-view-tracker',
|
||||
styleUrls: ['./view-tracker.component.scss'],
|
||||
templateUrl: './view-tracker.component.html',
|
||||
})
|
||||
export class ViewTrackerComponent implements OnInit {
|
||||
@Input() object:DSpaceObject;
|
||||
|
||||
constructor(
|
||||
public angulartics2:Angulartics2
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.angulartics2.eventTrack.next({
|
||||
action: 'pageView',
|
||||
properties: {object: this.object},
|
||||
});
|
||||
}
|
||||
}
|
36
src/app/statistics/statistics.module.ts
Normal file
36
src/app/statistics/statistics.module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ViewTrackerComponent } from './angulartics/dspace/view-tracker.component';
|
||||
import { StatisticsService } from './statistics.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CoreModule.forRoot(),
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
ViewTrackerComponent,
|
||||
],
|
||||
exports: [
|
||||
ViewTrackerComponent,
|
||||
],
|
||||
providers: [
|
||||
StatisticsService
|
||||
]
|
||||
})
|
||||
/**
|
||||
* This module handles the statistics
|
||||
*/
|
||||
export class StatisticsModule {
|
||||
static forRoot():ModuleWithProviders {
|
||||
return {
|
||||
ngModule: StatisticsModule,
|
||||
providers: [
|
||||
StatisticsService
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
145
src/app/statistics/statistics.service.spec.ts
Normal file
145
src/app/statistics/statistics.service.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { StatisticsService } from './statistics.service';
|
||||
import { RequestService } from '../core/data/request.service';
|
||||
import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub';
|
||||
import { getMockRequestService } from '../shared/mocks/mock-request.service';
|
||||
import { TrackRequest } from './track-request.model';
|
||||
import { ResourceType } from '../core/shared/resource-type';
|
||||
import { SearchOptions } from '../+search-page/search-options.model';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||
|
||||
describe('StatisticsService', () => {
|
||||
let service:StatisticsService;
|
||||
let requestService:jasmine.SpyObj<RequestService>;
|
||||
const restURL = 'https://rest.api';
|
||||
const halService:any = new HALEndpointServiceStub(restURL);
|
||||
|
||||
function initTestService() {
|
||||
return new StatisticsService(
|
||||
requestService,
|
||||
halService,
|
||||
);
|
||||
}
|
||||
|
||||
describe('trackViewEvent', () => {
|
||||
requestService = getMockRequestService();
|
||||
service = initTestService();
|
||||
|
||||
it('should send a request to track an item view ', () => {
|
||||
const mockItem:any = {uuid: 'mock-item-uuid', type: 'item'};
|
||||
service.trackViewEvent(mockItem);
|
||||
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
|
||||
expect(request.body).toBeDefined('request.body');
|
||||
const body = JSON.parse(request.body);
|
||||
expect(body.targetId).toBe('mock-item-uuid');
|
||||
expect(body.targetType).toBe('item');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSearchEvent', () => {
|
||||
requestService = getMockRequestService();
|
||||
service = initTestService();
|
||||
|
||||
const mockSearch:any = new SearchOptions({
|
||||
query: 'mock-query',
|
||||
});
|
||||
|
||||
const page = {
|
||||
size: 10,
|
||||
totalElements: 248,
|
||||
totalPages: 25,
|
||||
number: 4
|
||||
};
|
||||
const sort = {by: 'search-field', order: 'ASC'};
|
||||
service.trackSearchEvent(mockSearch, page, sort);
|
||||
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
|
||||
const body = JSON.parse(request.body);
|
||||
|
||||
it('should specify the right query', () => {
|
||||
expect(body.query).toBe('mock-query');
|
||||
});
|
||||
|
||||
it('should specify the pagination info', () => {
|
||||
expect(body.page).toEqual({
|
||||
size: 10,
|
||||
totalElements: 248,
|
||||
totalPages: 25,
|
||||
number: 4
|
||||
});
|
||||
});
|
||||
|
||||
it('should specify the sort options', () => {
|
||||
expect(body.sort).toEqual({
|
||||
by: 'search-field',
|
||||
order: 'asc'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSearchEvent with optional parameters', () => {
|
||||
requestService = getMockRequestService();
|
||||
service = initTestService();
|
||||
|
||||
const mockSearch:any = new SearchOptions({
|
||||
query: 'mock-query',
|
||||
configuration: 'mock-configuration',
|
||||
dsoType: DSpaceObjectType.ITEM,
|
||||
scope: 'mock-scope'
|
||||
});
|
||||
|
||||
const page = {
|
||||
size: 10,
|
||||
totalElements: 248,
|
||||
totalPages: 25,
|
||||
number: 4
|
||||
};
|
||||
const sort = {by: 'search-field', order: 'ASC'};
|
||||
const filters = [
|
||||
{
|
||||
filter: 'title',
|
||||
operator: 'notcontains',
|
||||
value: 'dolor sit',
|
||||
label: 'dolor sit'
|
||||
},
|
||||
{
|
||||
filter: 'author',
|
||||
operator: 'authority',
|
||||
value: '9zvxzdm4qru17or5a83wfgac',
|
||||
label: 'Amet, Consectetur'
|
||||
}
|
||||
];
|
||||
service.trackSearchEvent(mockSearch, page, sort, filters);
|
||||
const request:TrackRequest = requestService.configure.calls.mostRecent().args[0];
|
||||
const body = JSON.parse(request.body);
|
||||
|
||||
it('should specify the dsoType', () => {
|
||||
expect(body.dsoType).toBe('item');
|
||||
});
|
||||
|
||||
it('should specify the scope', () => {
|
||||
expect(body.scope).toBe('mock-scope');
|
||||
});
|
||||
|
||||
it('should specify the configuration', () => {
|
||||
expect(body.configuration).toBe('mock-configuration');
|
||||
});
|
||||
|
||||
it('should specify the filters', () => {
|
||||
expect(isEqual(body.appliedFilters, [
|
||||
{
|
||||
filter: 'title',
|
||||
operator: 'notcontains',
|
||||
value: 'dolor sit',
|
||||
label: 'dolor sit'
|
||||
},
|
||||
{
|
||||
filter: 'author',
|
||||
operator: 'authority',
|
||||
value: '9zvxzdm4qru17or5a83wfgac',
|
||||
label: 'Amet, Consectetur'
|
||||
}
|
||||
])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
93
src/app/statistics/statistics.service.ts
Normal file
93
src/app/statistics/statistics.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { RequestService } from '../core/data/request.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { TrackRequest } from './track-request.model';
|
||||
import { SearchOptions } from '../+search-page/search-options.model';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { HALEndpointService } from '../core/shared/hal-endpoint.service';
|
||||
import { RestRequest } from '../core/data/request.models';
|
||||
|
||||
/**
|
||||
* The statistics service
|
||||
*/
|
||||
@Injectable()
|
||||
export class StatisticsService {
|
||||
|
||||
constructor(
|
||||
protected requestService:RequestService,
|
||||
protected halService:HALEndpointService,
|
||||
) {
|
||||
}
|
||||
|
||||
private sendEvent(linkPath:string, body:any) {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
this.halService.getEndpoint(linkPath).pipe(
|
||||
map((endpoint:string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))),
|
||||
take(1) // otherwise the previous events will fire again
|
||||
).subscribe((request:RestRequest) => this.requestService.configure(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* To track a page view
|
||||
* @param dso: The dso which was viewed
|
||||
*/
|
||||
trackViewEvent(dso:DSpaceObject) {
|
||||
this.sendEvent('/statistics/viewevents', {
|
||||
targetId: dso.uuid,
|
||||
targetType: (dso as any).type
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* To track a search
|
||||
* @param searchOptions: The query, scope, dsoType and configuration of the search. Filters from this object are ignored in favor of the filters parameter of this method.
|
||||
* @param page: An object that describes the pagination status
|
||||
* @param sort: An object that describes the sort status
|
||||
* @param filters: An array of search filters used to filter the result set
|
||||
*/
|
||||
trackSearchEvent(
|
||||
searchOptions:SearchOptions,
|
||||
page:{ size:number, totalElements:number, totalPages:number, number:number },
|
||||
sort:{ by:string, order:string },
|
||||
filters?:Array<{ filter:string, operator:string, value:string, label:string }>
|
||||
) {
|
||||
const body = {
|
||||
query: searchOptions.query,
|
||||
page: {
|
||||
size: page.size,
|
||||
totalElements: page.totalElements,
|
||||
totalPages: page.totalPages,
|
||||
number: page.number
|
||||
},
|
||||
sort: {
|
||||
by: sort.by,
|
||||
order: sort.order.toLowerCase()
|
||||
},
|
||||
};
|
||||
if (hasValue(searchOptions.configuration)) {
|
||||
Object.assign(body, {configuration: searchOptions.configuration})
|
||||
}
|
||||
if (hasValue(searchOptions.dsoType)) {
|
||||
Object.assign(body, {dsoType: searchOptions.dsoType.toLowerCase()})
|
||||
}
|
||||
if (hasValue(searchOptions.scope)) {
|
||||
Object.assign(body, {scope: searchOptions.scope})
|
||||
}
|
||||
if (isNotEmpty(filters)) {
|
||||
const bodyFilters = [];
|
||||
for (let i = 0, arrayLength = filters.length; i < arrayLength; i++) {
|
||||
const filter = filters[i];
|
||||
bodyFilters.push({
|
||||
filter: filter.filter,
|
||||
operator: filter.operator,
|
||||
value: filter.value,
|
||||
label: filter.label
|
||||
})
|
||||
}
|
||||
Object.assign(body, {appliedFilters: bodyFilters})
|
||||
}
|
||||
this.sendEvent('/statistics/searchevents', body);
|
||||
}
|
||||
|
||||
}
|
4
src/app/statistics/track-request.model.ts
Normal file
4
src/app/statistics/track-request.model.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PostRequest } from '../core/data/request.models';
|
||||
|
||||
export class TrackRequest extends PostRequest {
|
||||
}
|
@@ -21,6 +21,8 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { SubmissionService } from '../../app/submission/submission.service';
|
||||
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||
import { StatisticsModule } from '../../app/statistics/statistics.module';
|
||||
|
||||
export const REQ_KEY = makeStateKey<string>('req');
|
||||
|
||||
@@ -47,7 +49,8 @@ export function getRequest(transferState: TransferState): any {
|
||||
preloadingStrategy:
|
||||
IdlePreload
|
||||
}),
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics]),
|
||||
StatisticsModule.forRoot(),
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics, Angulartics2DSpace]),
|
||||
BrowserAnimationsModule,
|
||||
DSpaceBrowserTransferStateModule,
|
||||
TranslateModule.forRoot({
|
||||
|
@@ -22,6 +22,8 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { AngularticsMock } from '../../app/shared/mocks/mock-angulartics.service';
|
||||
import { SubmissionService } from '../../app/submission/submission.service';
|
||||
import { ServerSubmissionService } from '../../app/submission/server-submission.service';
|
||||
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
|
||||
export function createTranslateLoader() {
|
||||
return new TranslateJson5UniversalLoader('dist/assets/i18n/', '.json5');
|
||||
@@ -45,6 +47,7 @@ export function createTranslateLoader() {
|
||||
deps: []
|
||||
}
|
||||
}),
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics, Angulartics2DSpace]),
|
||||
ServerModule,
|
||||
AppModule
|
||||
],
|
||||
@@ -53,6 +56,10 @@ export function createTranslateLoader() {
|
||||
provide: Angulartics2GoogleAnalytics,
|
||||
useClass: AngularticsMock
|
||||
},
|
||||
{
|
||||
provide: Angulartics2DSpace,
|
||||
useClass: AngularticsMock
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useClass: ServerAuthService
|
||||
|
Reference in New Issue
Block a user