mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge remote-tracking branch 'remotes/origin/master' into notifications
# Conflicts: # src/app/app.component.ts # src/app/core/core.module.ts
This commit is contained in:
@@ -239,6 +239,7 @@ dspace-angular
|
|||||||
├── config * Folder for configuration files
|
├── config * Folder for configuration files
|
||||||
│ ├── environment.default.js * Default configuration files
|
│ ├── environment.default.js * Default configuration files
|
||||||
│ └── environment.test.js * Test configuration files
|
│ └── environment.test.js * Test configuration files
|
||||||
|
├── docs * Folder for documentation
|
||||||
├── e2e * Folder for e2e test files
|
├── e2e * Folder for e2e test files
|
||||||
│ ├── app.e2e-spec.ts *
|
│ ├── app.e2e-spec.ts *
|
||||||
│ ├── app.po.ts *
|
│ ├── app.po.ts *
|
||||||
@@ -380,6 +381,11 @@ This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the e
|
|||||||
|
|
||||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
||||||
|
|
||||||
|
Further Documentation
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
See [`./docs`](docs) for further documentation.
|
||||||
|
|
||||||
Frequently asked questions
|
Frequently asked questions
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
@@ -18,7 +18,8 @@ module.exports = {
|
|||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
// NOTE: how long should objects be cached for by default
|
// NOTE: how long should objects be cached for by default
|
||||||
msToLive: 15 * 60 * 1000, // 15 minute
|
msToLive: 15 * 60 * 1000, // 15 minutes
|
||||||
|
// msToLive: 1000, // 15 minutes
|
||||||
control: 'max-age=60' // revalidate browser
|
control: 'max-age=60' // revalidate browser
|
||||||
},
|
},
|
||||||
// Notifications
|
// Notifications
|
||||||
@@ -38,6 +39,8 @@ module.exports = {
|
|||||||
async: true,
|
async: true,
|
||||||
time: false
|
time: false
|
||||||
},
|
},
|
||||||
|
// Google Analytics tracking id
|
||||||
|
gaTrackingId: '',
|
||||||
// Log directory
|
// Log directory
|
||||||
logDirectory: '.',
|
logDirectory: '.',
|
||||||
// NOTE: will log all redux actions and transfers in console
|
// NOTE: will log all redux actions and transfers in console
|
||||||
|
10
docs/Configuration.md
Normal file
10
docs/Configuration.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Supporting analytics services other than Google Analytics
|
||||||
|
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
||||||
|
|
||||||
|
Angulartics can be configured to work with a number of other services besides Google Analytics as well, e.g. [Piwik](https://github.com/angulartics/angulartics2/tree/master/src/lib/providers/piwik), [Google Tag Manager](https://github.com/angulartics/angulartics2/tree/master/src/lib/providers/gtm), or [Azure Application Insights](https://azure.microsoft.com/en-us/services/application-insights/) to name a few.
|
||||||
|
|
||||||
|
In order to start using one of these services, select it from the [Angulartics Providers page](https://angulartics.github.io/angulartics2/#providers), and follow the instructions on how to configure it.
|
||||||
|
|
||||||
|
The Google Analytics script was added in [`main.browser.ts`](https://github.com/DSpace/dspace-angular/blob/ff04760f4af91ac3e7add5e7424a46cb2439e874/src/main.browser.ts#L33) instead of the `<head>` tag in `index.html` to ensure events get sent when the page is shown in a client's browser, and not when it's rendered on the universal server. Likely you'll want to do the same when adding a new service.
|
@@ -51,7 +51,7 @@ module.exports = function (config) {
|
|||||||
*/
|
*/
|
||||||
files: [{
|
files: [{
|
||||||
pattern: './spec-bundle.js',
|
pattern: './spec-bundle.js',
|
||||||
watched: false
|
watched: false,
|
||||||
}],
|
}],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"config",
|
"config",
|
||||||
"src/index.html"
|
"src/index.html"
|
||||||
],
|
],
|
||||||
"ext": "js ts json html"
|
"ext": "js ts json html",
|
||||||
|
"delay": "50"
|
||||||
}
|
}
|
||||||
|
@@ -87,6 +87,7 @@
|
|||||||
"@ngx-translate/core": "9.1.1",
|
"@ngx-translate/core": "9.1.1",
|
||||||
"@ngx-translate/http-loader": "2.0.1",
|
"@ngx-translate/http-loader": "2.0.1",
|
||||||
"angular-idle-preload": "2.0.4",
|
"angular-idle-preload": "2.0.4",
|
||||||
|
"angulartics2": "^5.2.0",
|
||||||
"body-parser": "1.18.2",
|
"body-parser": "1.18.2",
|
||||||
"bootstrap": "^4.0.0",
|
"bootstrap": "^4.0.0",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
@@ -116,6 +117,7 @@
|
|||||||
"@angular/compiler-cli": "^5.2.5",
|
"@angular/compiler-cli": "^5.2.5",
|
||||||
"@ngrx/store-devtools": "^5.1.0",
|
"@ngrx/store-devtools": "^5.1.0",
|
||||||
"@ngtools/webpack": "^1.10.0",
|
"@ngtools/webpack": "^1.10.0",
|
||||||
|
"@types/acorn": "^4.0.3",
|
||||||
"@types/cookie-parser": "1.4.1",
|
"@types/cookie-parser": "1.4.1",
|
||||||
"@types/deep-freeze": "0.1.1",
|
"@types/deep-freeze": "0.1.1",
|
||||||
"@types/express": "^4.11.1",
|
"@types/express": "^4.11.1",
|
||||||
|
@@ -56,6 +56,10 @@
|
|||||||
"detail": "{{ range }} of {{ total }}"
|
"detail": "{{ range }} of {{ total }}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sorting": {
|
||||||
|
"ASC": "Ascending",
|
||||||
|
"DESC": "Descending"
|
||||||
|
},
|
||||||
"title": "DSpace",
|
"title": "DSpace",
|
||||||
"404": {
|
"404": {
|
||||||
"help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
"help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
||||||
@@ -80,7 +84,8 @@
|
|||||||
"search_dspace": "Search DSpace"
|
"search_dspace": "Search DSpace"
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"head": "Search Results"
|
"head": "Search Results",
|
||||||
|
"no-results": "There were no results for this search"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"close": "Back to results",
|
"close": "Back to results",
|
||||||
@@ -117,9 +122,13 @@
|
|||||||
"placeholder": "Subject",
|
"placeholder": "Subject",
|
||||||
"head": "Subject"
|
"head": "Subject"
|
||||||
},
|
},
|
||||||
"date": {
|
"dateIssued": {
|
||||||
"placeholder": "Date",
|
"placeholder": "Date",
|
||||||
"head": "Date"
|
"head": "Date"
|
||||||
|
},
|
||||||
|
"has_content_in_original_bundle": {
|
||||||
|
"placeholder": "Has files",
|
||||||
|
"head": "Has files"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -48,7 +48,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
this.paginationConfig.id = 'collection-page-pagination';
|
this.paginationConfig.id = 'collection-page-pagination';
|
||||||
this.paginationConfig.pageSize = 5;
|
this.paginationConfig.pageSize = 5;
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions();
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export class TopLevelCommunityListComponent {
|
|||||||
this.config.id = 'top-level-pagination';
|
this.config.id = 'top-level-pagination';
|
||||||
this.config.pageSize = 5;
|
this.config.pageSize = 5;
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions();
|
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
|
|
||||||
this.updatePage({
|
this.updatePage({
|
||||||
page: this.config.currentPage,
|
page: this.config.currentPage,
|
||||||
|
13
src/app/+search-page/normalized-search-result.model.ts
Normal file
13
src/app/+search-page/normalized-search-result.model.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { autoserialize } from 'cerialize';
|
||||||
|
import { Metadatum } from '../core/shared/metadatum.model';
|
||||||
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
|
export class NormalizedSearchResult implements ListableObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
dspaceObject: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
hitHighlights: Metadatum[];
|
||||||
|
|
||||||
|
}
|
20
src/app/+search-page/paginated-search-options.model.ts
Normal file
20
src/app/+search-page/paginated-search-options.model.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { SearchOptions } from './search-options.model';
|
||||||
|
|
||||||
|
export class PaginatedSearchOptions extends SearchOptions {
|
||||||
|
pagination?: PaginationComponentOptions;
|
||||||
|
sort?: SortOptions;
|
||||||
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
|
if (isNotEmpty(this.sort)) {
|
||||||
|
args.push(`sort=${this.sort.field},${this.sort.direction}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(this.pagination)) {
|
||||||
|
args.push(`page=${this.pagination.currentPage - 1}`);
|
||||||
|
args.push(`size=${this.pagination.pageSize}`);
|
||||||
|
}
|
||||||
|
return super.toRestUrl(url, args);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,26 +2,29 @@
|
|||||||
<div class="filters">
|
<div class="filters">
|
||||||
<a *ngFor="let value of selectedValues" class="d-block"
|
<a *ngFor="let value of selectedValues" class="d-block"
|
||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="getQueryParamsWithout(value) | async">
|
[queryParams]="getRemoveParams(value)" queryParamsHandling="merge">
|
||||||
<input type="checkbox" [checked]="true"/>
|
<input type="checkbox" [checked]="true"/>
|
||||||
<span class="filter-value">{{value}}</span>
|
<span class="filter-value">{{value}}</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngFor="let value of filterValues; let i=index" class="d-block clearfix"
|
<ng-container *ngFor="let page of (filterValues$ | async)">
|
||||||
[routerLink]="[getSearchLink()]"
|
<ng-container *ngFor="let value of (page | async)?.payload.page; let i=index">
|
||||||
[queryParams]="getQueryParamsWith(value.value) | async">
|
<a *ngIf="!selectedValues.includes(value.value)" class="d-block clearfix"
|
||||||
<ng-template [ngIf]="i < (facetCount | async)">
|
[routerLink]="[getSearchLink()]"
|
||||||
|
[queryParams]="getAddParams(value.value)" queryParamsHandling="merge" >
|
||||||
<input type="checkbox" [checked]="false"/>
|
<input type="checkbox" [checked]="false"/>
|
||||||
<span class="filter-value">{{value.value}}</span>
|
<span class="filter-value">{{value.value}}</span>
|
||||||
<span class="float-right filter-value-count">
|
<span class="float-right filter-value-count">
|
||||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</a>
|
||||||
</a>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
<a class="float-left" *ngIf="filterValues.length > (facetCount | async)"
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
(click)="showMore()">{{"search.filters.filter.show-more"
|
(click)="showMore()">{{"search.filters.filter.show-more"
|
||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
<a class="float-right" *ngIf="(currentPage | async) > 1" (click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
|
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +32,7 @@
|
|||||||
[action]="getCurrentUrl()">
|
[action]="getCurrentUrl()">
|
||||||
<input type="text" [(ngModel)]="filter" [name]="filterConfig.paramName" class="form-control"
|
<input type="text" [(ngModel)]="filter" [name]="filterConfig.paramName" class="form-control"
|
||||||
aria-label="New filter input"
|
aria-label="New filter input"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"/>
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate" [ngModelOptions]="{standalone: true}"/>
|
||||||
<input type="submit" class="d-none"/>
|
<input type="submit" class="d-none"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
@@ -10,6 +10,15 @@ import { FilterType } from '../../../search-service/filter-type.model';
|
|||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { SearchService } from '../../../search-service/search.service';
|
||||||
|
import { SearchServiceStub } from '../../../../shared/testing/search-service-stub';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
|
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
describe('SearchFacetFilterComponent', () => {
|
describe('SearchFacetFilterComponent', () => {
|
||||||
let comp: SearchFacetFilterComponent;
|
let comp: SearchFacetFilterComponent;
|
||||||
@@ -40,29 +49,35 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
search: ''
|
search: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const searchLink = '/search';
|
||||||
|
const selectedValues = [value1, value2];
|
||||||
let filterService;
|
let filterService;
|
||||||
const page = Observable.of(0)
|
let searchService;
|
||||||
|
let router;
|
||||||
|
const page = Observable.of(0);
|
||||||
|
|
||||||
|
const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values)));
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||||
declarations: [SearchFacetFilterComponent],
|
declarations: [SearchFacetFilterComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||||
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
{
|
{
|
||||||
provide: SearchFilterService,
|
provide: SearchFilterService, useValue: {
|
||||||
useValue: {
|
isFilterActiveWithValue: (paramName: string, filterValue: string) => true,
|
||||||
isFilterActiveWithValue: (paramName: string, filterValue: string) => true,
|
getPage: (paramName: string) => page,
|
||||||
getQueryParamsWith: (paramName: string, filterValue: string) => '',
|
/* tslint:disable:no-empty */
|
||||||
getQueryParamsWithout: (paramName: string, filterValue: string) => '',
|
incrementPage: (filterName: string) => {
|
||||||
getPage: (paramName: string) => page,
|
},
|
||||||
/* tslint:disable:no-empty */
|
resetPage: (filterName: string) => {
|
||||||
incrementPage: (filterName: string) => {
|
},
|
||||||
},
|
getSearchOptions: () => Observable.of({}),
|
||||||
resetPage: (filterName: string) => {
|
/* tslint:enable:no-empty */
|
||||||
},
|
}
|
||||||
/* tslint:enable:no-empty */
|
}
|
||||||
searchLink: '/search',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFacetFilterComponent, {
|
}).overrideComponent(SearchFacetFilterComponent, {
|
||||||
@@ -74,8 +89,13 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
fixture = TestBed.createComponent(SearchFacetFilterComponent);
|
fixture = TestBed.createComponent(SearchFacetFilterComponent);
|
||||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
comp.filterConfig = mockFilterConfig;
|
comp.filterConfig = mockFilterConfig;
|
||||||
comp.filterValues = values;
|
comp.filterValues = [mockValues];
|
||||||
|
comp.filterValues$ = new BehaviorSubject(comp.filterValues);
|
||||||
|
comp.selectedValues = selectedValues;
|
||||||
filterService = (comp as any).filterService;
|
filterService = (comp as any).filterService;
|
||||||
|
searchService = (comp as any).searchService;
|
||||||
|
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
|
||||||
|
router = (comp as any).router;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,62 +117,21 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the value of the searchLink variable in the filter service', () => {
|
it('should return the value of the searchLink variable in the filter service', () => {
|
||||||
expect(link).toEqual(filterService.searchLink);
|
expect(link).toEqual(searchLink);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the getQueryParamsWith method is called wih a value', () => {
|
describe('when the getAddParams method is called wih a value', () => {
|
||||||
beforeEach(() => {
|
it('should return the selectedValue list with the new parameter value', () => {
|
||||||
spyOn(filterService, 'getQueryParamsWith');
|
const result = comp.getAddParams(value3);
|
||||||
comp.getQueryParamsWith(values[1].value);
|
expect(result[mockFilterConfig.paramName]).toEqual([value1, value2, value3]);
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getQueryParamsWith on the filterService with the correct filter parameter name and the passed value', () => {
|
|
||||||
expect(filterService.getQueryParamsWith).toHaveBeenCalledWith(mockFilterConfig, values[1].value)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the getQueryParamsWithout method is called wih a value', () => {
|
describe('when the getRemoveParams method is called wih a value', () => {
|
||||||
beforeEach(() => {
|
it('should return the selectedValue list with the parameter value left out', () => {
|
||||||
spyOn(filterService, 'getQueryParamsWithout');
|
const result = comp.getRemoveParams(value1);
|
||||||
comp.getQueryParamsWithout(values[1].value);
|
expect(result[mockFilterConfig.paramName]).toEqual([value2]);
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getQueryParamsWithout on the filterService with the correct filter parameter name and the passed value', () => {
|
|
||||||
expect(filterService.getQueryParamsWithout).toHaveBeenCalledWith(mockFilterConfig, values[1].value)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the facetCount method is triggered when there are less items than the amount of pages should display', () => {
|
|
||||||
let count: Observable<number>;
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.currentPage = Observable.of(3);
|
|
||||||
// 2 x 3 = 6, there are only 3 values
|
|
||||||
count = comp.facetCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the correct number of items shown (this equals the total amount of values for this filter)', () => {
|
|
||||||
const sub = count.subscribe((c) => expect(c).toBe(values.length));
|
|
||||||
sub.unsubscribe();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the facetCount method is triggered when there are more items than the amount of pages should display', () => {
|
|
||||||
let count: Observable<number>;
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.currentPage = Observable.of(1);
|
|
||||||
// 2 x 1 = 2, there are more than 2 (3) items
|
|
||||||
count = comp.facetCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the correct number of items shown (this equals the page count x page size)', () => {
|
|
||||||
const sub = count.subscribe((c) => {
|
|
||||||
const subsub = comp.currentPage.subscribe((currentPage) => {
|
|
||||||
expect(c).toBe(currentPage * mockFilterConfig.pageSize);
|
|
||||||
});
|
|
||||||
subsub.unsubscribe()
|
|
||||||
});
|
|
||||||
sub.unsubscribe();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,7 +153,7 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call resetPage on the filterService with the correct filter parameter name', () => {
|
it('should call resetPage on the filterService with the correct filter parameter name', () => {
|
||||||
expect(filterService.resetPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
expect(filterService.resetPage).toHaveBeenCalledWith(mockFilterConfig.name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,4 +167,76 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
expect(filterService.getPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
expect(filterService.getPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentUrl method is called', () => {
|
||||||
|
const url = 'test.url/test'
|
||||||
|
beforeEach(() => {
|
||||||
|
router.navigateByUrl(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getPage on the filterService with the correct filter parameter name', () => {
|
||||||
|
expect(router.url).toEqual(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the onSubmit method is called with data', () => {
|
||||||
|
const searchUrl = '/search/path';
|
||||||
|
const testValue = 'test';
|
||||||
|
const data = { [mockFilterConfig.paramName]: testValue };
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'getSearchLink').and.returnValue(searchUrl);
|
||||||
|
comp.onSubmit(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call navigate on the router with the right searchlink and parameters', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith([searchUrl], {
|
||||||
|
queryParams: { [mockFilterConfig.paramName]: [...selectedValues, testValue] },
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updateFilterValueList is called', () => {
|
||||||
|
const cPage = 10;
|
||||||
|
const searchOptions = new SearchOptions();
|
||||||
|
beforeEach(() => {
|
||||||
|
// spyOn(searchService, 'getFacetValuesFor'); Already spied upon
|
||||||
|
comp.currentPage = Observable.of(cPage);
|
||||||
|
comp.updateFilterValueList(searchOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getFacetValuesFor on the searchService with the correct parameters', () => {
|
||||||
|
expect(searchService.getFacetValuesFor).toHaveBeenCalledWith(mockFilterConfig, cPage, searchOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updateFilterValueList is called and pageChange is set to true', () => {
|
||||||
|
const searchOptions = new SearchOptions();
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.pageChange = true;
|
||||||
|
spyOn(comp, 'showFirstPageOnly');
|
||||||
|
comp.updateFilterValueList(searchOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call showFirstPageOnly on the component', () => {
|
||||||
|
expect(comp.showFirstPageOnly).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set pageChange to false', () => {
|
||||||
|
expect(comp.pageChange).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updateFilterValueList is called and pageChange is set to false', () => {
|
||||||
|
const searchOptions = new SearchOptions();
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.pageChange = false;
|
||||||
|
spyOn(comp, 'showFirstPageOnly');
|
||||||
|
comp.updateFilterValueList(searchOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call showFirstPageOnly on the component', () => {
|
||||||
|
expect(comp.showFirstPageOnly).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { Params, Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SearchFilterService } from '../search-filter.service';
|
import { SearchFilterService } from '../search-filter.service';
|
||||||
import { isNotEmpty } from '../../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { SearchService } from '../../../search-service/search.service';
|
||||||
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -15,21 +21,43 @@ import { isNotEmpty } from '../../../../shared/empty.util';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
styleUrls: ['./search-facet-filter.component.scss'],
|
styleUrls: ['./search-facet-filter.component.scss'],
|
||||||
templateUrl: './search-facet-filter.component.html',
|
templateUrl: './search-facet-filter.component.html'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class SearchFacetFilterComponent implements OnInit {
|
export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||||
@Input() filterValues: FacetValue[];
|
|
||||||
@Input() filterConfig: SearchFilterConfig;
|
@Input() filterConfig: SearchFilterConfig;
|
||||||
@Input() selectedValues: string[];
|
@Input() selectedValues: string[];
|
||||||
|
filterValues: Array<Observable<RemoteData<PaginatedList<FacetValue>>>> = [];
|
||||||
|
filterValues$: BehaviorSubject<any> = new BehaviorSubject(this.filterValues);
|
||||||
currentPage: Observable<number>;
|
currentPage: Observable<number>;
|
||||||
|
isLastPage$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
filter: string;
|
filter: string;
|
||||||
|
pageChange = false;
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
constructor(private filterService: SearchFilterService, private router: Router) {
|
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.currentPage = this.filterService.getPage(this.filterConfig.name);
|
this.currentPage = this.getCurrentPage();
|
||||||
|
this.currentPage.distinctUntilChanged().subscribe((page) => this.pageChange = true);
|
||||||
|
this.filterService.getSearchOptions().distinctUntilChanged().subscribe((options) => this.updateFilterValueList(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilterValueList(options: SearchOptions) {
|
||||||
|
if (!this.pageChange) {
|
||||||
|
this.showFirstPageOnly();
|
||||||
|
}
|
||||||
|
this.pageChange = false;
|
||||||
|
|
||||||
|
this.unsubscribe();
|
||||||
|
this.sub = this.currentPage.distinctUntilChanged().map((page) => {
|
||||||
|
return this.searchService.getFacetValuesFor(this.filterConfig, page, options);
|
||||||
|
}).subscribe((newValues$) => {
|
||||||
|
this.filterValues = [...this.filterValues, newValues$];
|
||||||
|
this.filterValues$.next(this.filterValues);
|
||||||
|
newValues$.first().subscribe((rd) => this.isLastPage$.next(hasNoValue(rd.payload.next)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isChecked(value: FacetValue): Observable<boolean> {
|
isChecked(value: FacetValue): Observable<boolean> {
|
||||||
@@ -37,23 +65,7 @@ export class SearchFacetFilterComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSearchLink() {
|
getSearchLink() {
|
||||||
return this.filterService.searchLink;
|
return this.searchService.getSearchLink();
|
||||||
}
|
|
||||||
|
|
||||||
getQueryParamsWith(value: string): Observable<Params> {
|
|
||||||
return this.filterService.getQueryParamsWith(this.filterConfig, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueryParamsWithout(value: string): Observable<Params> {
|
|
||||||
return this.filterService.getQueryParamsWithout(this.filterConfig, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
get facetCount(): Observable<number> {
|
|
||||||
const resultCount = this.filterValues.length;
|
|
||||||
return this.currentPage.map((page: number) => {
|
|
||||||
const max = page * this.filterConfig.pageSize;
|
|
||||||
return max > resultCount ? resultCount : max;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showMore() {
|
showMore() {
|
||||||
@@ -61,6 +73,7 @@ export class SearchFacetFilterComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showFirstPageOnly() {
|
showFirstPageOnly() {
|
||||||
|
this.filterValues = [];
|
||||||
this.filterService.resetPage(this.filterConfig.name);
|
this.filterService.resetPage(this.filterConfig.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +87,39 @@ export class SearchFacetFilterComponent implements OnInit {
|
|||||||
|
|
||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
const sub = this.getQueryParamsWith(data[this.filterConfig.paramName]).first().subscribe((params) => {
|
this.router.navigate([this.getSearchLink()], {
|
||||||
this.router.navigate([this.getSearchLink()], { queryParams: params }
|
queryParams:
|
||||||
);
|
{ [this.filterConfig.paramName]: [...this.selectedValues, data[this.filterConfig.paramName]] },
|
||||||
}
|
queryParamsHandling: 'merge'
|
||||||
);
|
});
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
sub.unsubscribe();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValue(o: any): boolean {
|
||||||
|
return hasValue(o);
|
||||||
|
}
|
||||||
|
getRemoveParams(value: string) {
|
||||||
|
return {
|
||||||
|
[this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value),
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddParams(value: string) {
|
||||||
|
return {
|
||||||
|
[this.filterConfig.paramName]: [...this.selectedValues, value],
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fa float-right"
|
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fa float-right"
|
||||||
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||||
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" class="search-filter-wrapper">
|
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" class="search-filter-wrapper">
|
||||||
<ds-search-facet-filter [filterConfig]="filter"
|
<ds-search-facet-filter [filterConfig]="filter" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
|
||||||
[filterValues]="(filterValues | async)?.payload" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -6,8 +6,7 @@ import { FacetValue } from '../../search-service/facet-value.model';
|
|||||||
import { SearchFilterService } from './search-filter.service';
|
import { SearchFilterService } from './search-filter.service';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { RouteService } from '../../../shared/route.service';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { first } from 'rxjs/operator/first';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -24,21 +23,18 @@ import { first } from 'rxjs/operator/first';
|
|||||||
|
|
||||||
export class SearchFilterComponent implements OnInit {
|
export class SearchFilterComponent implements OnInit {
|
||||||
@Input() filter: SearchFilterConfig;
|
@Input() filter: SearchFilterConfig;
|
||||||
filterValues: Observable<RemoteData<FacetValue[]>>;
|
|
||||||
|
|
||||||
constructor(private searchService: SearchService, private filterService: SearchFilterService) {
|
constructor(private filterService: SearchFilterService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.filterValues = this.searchService.getFacetValuesFor(this.filter.name);
|
this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => {
|
||||||
const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => {
|
|
||||||
if (this.filter.isOpenByDefault || isActive) {
|
if (this.filter.isOpenByDefault || isActive) {
|
||||||
this.initialExpand();
|
this.initialExpand();
|
||||||
} else {
|
} else {
|
||||||
this.initialCollapse();
|
this.initialCollapse();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sub.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
|
@@ -46,11 +46,11 @@ describe('SearchFilterService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchServiceStub: any = {
|
const searchServiceStub: any = {
|
||||||
searchLink: '/search'
|
uiSearchRoute: '/search'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new SearchFilterService(store, routeServiceStub, searchServiceStub);
|
service = new SearchFilterService(store, routeServiceStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the initialCollapse method is triggered', () => {
|
describe('when the initialCollapse method is triggered', () => {
|
||||||
@@ -159,28 +159,6 @@ describe('SearchFilterService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the getQueryParamsWithout method is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(routeServiceStub, 'removeQueryParameterValue');
|
|
||||||
service.getQueryParamsWithout(mockFilterConfig, value1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call removeQueryParameterValue on the route service with the same parameters', () => {
|
|
||||||
expect(routeServiceStub.removeQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the getQueryParamsWith method is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(routeServiceStub, 'addQueryParameterValue');
|
|
||||||
service.getQueryParamsWith(mockFilterConfig, value1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call addQueryParameterValue on the route service with the same parameters', () => {
|
|
||||||
expect(routeServiceStub.addQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the getSelectedValuesForFilter method is called', () => {
|
describe('when the getSelectedValuesForFilter method is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(routeServiceStub, 'getQueryParameterValues');
|
spyOn(routeServiceStub, 'getQueryParameterValues');
|
||||||
@@ -192,14 +170,4 @@ describe('SearchFilterService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the searchLink method is called', () => {
|
|
||||||
let link: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
link = service.searchLink;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the value of searchLink in the search service', () => {
|
|
||||||
expect(link).toEqual(searchServiceStub.searchLink);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
||||||
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@@ -10,10 +11,15 @@ import {
|
|||||||
SearchFilterInitialExpandAction, SearchFilterResetPageAction,
|
SearchFilterInitialExpandAction, SearchFilterResetPageAction,
|
||||||
SearchFilterToggleAction
|
SearchFilterToggleAction
|
||||||
} from './search-filter.actions';
|
} from './search-filter.actions';
|
||||||
import { hasValue, } from '../../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchService } from '../../search-service/search.service';
|
import { SearchService } from '../../search-service/search.service';
|
||||||
import { RouteService } from '../../../shared/route.service';
|
import { RouteService } from '../../../shared/route.service';
|
||||||
|
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SearchOptions } from '../../search-options.model';
|
||||||
|
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
||||||
|
|
||||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||||
|
|
||||||
@@ -21,8 +27,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
|||||||
export class SearchFilterService {
|
export class SearchFilterService {
|
||||||
|
|
||||||
constructor(private store: Store<SearchFiltersState>,
|
constructor(private store: Store<SearchFiltersState>,
|
||||||
private routeService: RouteService,
|
private routeService: RouteService) {
|
||||||
private searchService: SearchService) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
|
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
|
||||||
@@ -33,22 +38,91 @@ export class SearchFilterService {
|
|||||||
return this.routeService.hasQueryParam(paramName);
|
return this.routeService.hasQueryParam(paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryParamsWithout(filterConfig: SearchFilterConfig, value: string) {
|
getCurrentScope() {
|
||||||
return this.routeService.removeQueryParameterValue(filterConfig.paramName, value);
|
return this.routeService.getQueryParameterValue('scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryParamsWith(filterConfig: SearchFilterConfig, value: string) {
|
getCurrentQuery() {
|
||||||
return this.routeService.addQueryParameterValue(filterConfig.paramName, value);
|
return this.routeService.getQueryParameterValue('query');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
|
||||||
|
const page$ = this.routeService.getQueryParameterValue('page');
|
||||||
|
const size$ = this.routeService.getQueryParameterValue('pageSize');
|
||||||
|
return Observable.combineLatest(page$, size$, (page, size) => {
|
||||||
|
return Object.assign(new PaginationComponentOptions(), pagination, {
|
||||||
|
currentPage: page || 1,
|
||||||
|
pageSize: size || pagination.pageSize
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
|
||||||
|
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
|
||||||
|
const sortField$ = this.routeService.getQueryParameterValue('sortField');
|
||||||
|
return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => {
|
||||||
|
const field = sortField || defaultSort.field;
|
||||||
|
const direction = SortDirection[sortDirection] || defaultSort.direction;
|
||||||
|
return new SortOptions(field, direction)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentFilters() {
|
||||||
|
return this.routeService.getQueryParamsWithPrefix('f.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentView() {
|
||||||
|
return this.routeService.getQueryParameterValue('view');
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
|
||||||
|
return Observable.combineLatest(
|
||||||
|
this.getCurrentPagination(defaults.pagination),
|
||||||
|
this.getCurrentSort(defaults.sort),
|
||||||
|
this.getCurrentView(),
|
||||||
|
this.getCurrentScope(),
|
||||||
|
this.getCurrentQuery(),
|
||||||
|
this.getCurrentFilters()).pipe(
|
||||||
|
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
|
||||||
|
map(([pagination, sort, view, scope, query, filters]) => {
|
||||||
|
return Object.assign(new PaginatedSearchOptions(),
|
||||||
|
defaults,
|
||||||
|
{
|
||||||
|
pagination: pagination,
|
||||||
|
sort: sort,
|
||||||
|
view: view,
|
||||||
|
scope: scope || defaults.scope,
|
||||||
|
query: query,
|
||||||
|
filters: filters
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
|
||||||
|
return Observable.combineLatest(
|
||||||
|
this.getCurrentView(),
|
||||||
|
this.getCurrentScope(),
|
||||||
|
this.getCurrentQuery(),
|
||||||
|
this.getCurrentFilters(),
|
||||||
|
(view, scope, query, filters) => {
|
||||||
|
return Object.assign(new SearchOptions(),
|
||||||
|
defaults,
|
||||||
|
{
|
||||||
|
view: view,
|
||||||
|
scope: scope || defaults.scope,
|
||||||
|
query: query,
|
||||||
|
filters: filters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
||||||
return this.routeService.getQueryParameterValues(filterConfig.paramName);
|
return this.routeService.getQueryParameterValues(filterConfig.paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
get searchLink() {
|
|
||||||
return this.searchService.searchLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
isCollapsed(filterName: string): Observable<boolean> {
|
isCollapsed(filterName: string): Observable<boolean> {
|
||||||
return this.store.select(filterByNameSelector(filterName))
|
return this.store.select(filterByNameSelector(filterName))
|
||||||
.map((object: SearchFilterState) => {
|
.map((object: SearchFilterState) => {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<h3>{{"search.filters.head" | translate}}</h3>
|
<h3>{{"search.filters.head" | translate}}</h3>
|
||||||
<div *ngIf="(filters | async).hasSucceeded">
|
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||||
<div *ngFor="let filter of (filters | async).payload">
|
<div *ngFor="let filter of (filters | async).payload">
|
||||||
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
|
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="getClearFiltersQueryParams()" role="button">{{"search.filters.reset" | translate}}</a>
|
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
|
@@ -4,6 +4,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { SearchFiltersComponent } from './search-filters.component';
|
import { SearchFiltersComponent } from './search-filters.component';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@@ -22,6 +23,9 @@ describe('SearchFiltersComponent', () => {
|
|||||||
}
|
}
|
||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
};
|
};
|
||||||
|
const searchFilterServiceStub = jasmine.createSpyObj('SearchFilterService', {
|
||||||
|
getCurrentFilters: Observable.of({})
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -29,6 +33,7 @@ describe('SearchFiltersComponent', () => {
|
|||||||
declarations: [SearchFiltersComponent],
|
declarations: [SearchFiltersComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{ provide: SearchFilterService, useValue: searchFilterServiceStub },
|
||||||
|
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -44,17 +49,6 @@ describe('SearchFiltersComponent', () => {
|
|||||||
searchService = (comp as any).searchService;
|
searchService = (comp as any).searchService;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the getClearFiltersQueryParams method is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(searchService, 'getClearFiltersQueryParams');
|
|
||||||
comp.getClearFiltersQueryParams();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getClearFiltersQueryParams on the searchService', () => {
|
|
||||||
expect(searchService.getClearFiltersQueryParams).toHaveBeenCalled()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the getSearchLink method is called', () => {
|
describe('when the getSearchLink method is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(searchService, 'getSearchLink');
|
spyOn(searchService, 'getSearchLink');
|
||||||
|
@@ -3,6 +3,7 @@ import { SearchService } from '../search-service/search.service';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -18,12 +19,10 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
|
|
||||||
export class SearchFiltersComponent {
|
export class SearchFiltersComponent {
|
||||||
filters: Observable<RemoteData<SearchFilterConfig[]>>;
|
filters: Observable<RemoteData<SearchFilterConfig[]>>;
|
||||||
constructor(private searchService: SearchService) {
|
clearParams;
|
||||||
|
constructor(private searchService: SearchService, private filterService: SearchFilterService) {
|
||||||
this.filters = searchService.getConfig();
|
this.filters = searchService.getConfig();
|
||||||
}
|
this.clearParams = filterService.getCurrentFilters().map((filters) => {Object.keys(filters).forEach((f) => filters[f] = null); return filters;});
|
||||||
|
|
||||||
getClearFiltersQueryParams(): any {
|
|
||||||
return this.searchService.getClearFiltersQueryParams();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchLink() {
|
getSearchLink() {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import 'core-js/fn/object/entries';
|
||||||
|
|
||||||
export enum ViewMode {
|
export enum ViewMode {
|
||||||
List = 'list',
|
List = 'list',
|
||||||
@@ -7,7 +8,28 @@ export enum ViewMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SearchOptions {
|
export class SearchOptions {
|
||||||
pagination?: PaginationComponentOptions;
|
|
||||||
sort?: SortOptions;
|
|
||||||
view?: ViewMode = ViewMode.List;
|
view?: ViewMode = ViewMode.List;
|
||||||
|
scope?: string;
|
||||||
|
query?: string;
|
||||||
|
filters?: any;
|
||||||
|
|
||||||
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
|
|
||||||
|
if (isNotEmpty(this.query)) {
|
||||||
|
args.push(`query=${this.query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(this.scope)) {
|
||||||
|
args.push(`scope=${this.scope}`);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(this.filters)) {
|
||||||
|
Object.entries(this.filters).forEach(([key, values]) => {
|
||||||
|
values.forEach((value) => args.push(`${key}=${value},equals`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-page row">
|
<div class="search-page row">
|
||||||
<ds-search-sidebar *ngIf="!(isMobileView | async)" class="col-3 sidebar-md-sticky"
|
<ds-search-sidebar *ngIf="!(isMobileView$ | async)" class="col-3 sidebar-md-sticky"
|
||||||
id="search-sidebar"
|
id="search-sidebar"
|
||||||
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"></ds-search-sidebar>
|
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
<ds-search-form id="search-form"
|
<ds-search-form id="search-form"
|
||||||
[query]="query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(scopeObjectRDObs | async)?.payload"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentParams]="currentParams"
|
[currentUrl]="getSearchLink()"
|
||||||
[scopes]="(scopeListRDObs | async)?.payload?.page">
|
[scopes]="(scopeListRD$ | async)?.payload?.page">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left"
|
||||||
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12"
|
<ds-search-sidebar *ngIf="(isMobileView$ | async)" class="col-12"
|
||||||
id="search-sidebar-sm"
|
id="search-sidebar-sm"
|
||||||
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"
|
[resultCount]="(resultsRD$ | async)?.pageInfo?.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
@@ -29,13 +29,11 @@
|
|||||||
| translate}}
|
| translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-search-results [searchResults]="resultsRDObs | async"
|
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||||
[searchConfig]="searchOptions" [sortConfig]="sortConfig"></ds-search-results>
|
[searchConfig]="searchOptions$ | async" [sortConfig]="sortConfig"></ds-search-results>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,10 +4,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { Community } from '../core/shared/community.model';
|
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
@@ -17,6 +17,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
|
|
||||||
describe('SearchPageComponent', () => {
|
describe('SearchPageComponent', () => {
|
||||||
let comp: SearchPageComponent;
|
let comp: SearchPageComponent;
|
||||||
@@ -32,14 +33,20 @@ describe('SearchPageComponent', () => {
|
|||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
const sort: SortOptions = new SortOptions();
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = Observable.of(['test', 'data']);
|
const mockResults = Observable.of(['test', 'data']);
|
||||||
const searchServiceStub = {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
searchOptions:{ pagination: pagination, sort: sort },
|
search: mockResults,
|
||||||
search: () => mockResults
|
getSearchLink: '/search'
|
||||||
};
|
});
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
|
const paginatedSearchOptions = {
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam,
|
||||||
|
pagination,
|
||||||
|
sort
|
||||||
|
};
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
queryParams: Observable.of({
|
queryParams: Observable.of({
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
@@ -50,20 +57,8 @@ describe('SearchPageComponent', () => {
|
|||||||
isCollapsed: Observable.of(true),
|
isCollapsed: Observable.of(true),
|
||||||
collapse: () => this.isCollapsed = Observable.of(true),
|
collapse: () => this.isCollapsed = Observable.of(true),
|
||||||
expand: () => this.isCollapsed = Observable.of(false)
|
expand: () => this.isCollapsed = Observable.of(false)
|
||||||
}
|
|
||||||
|
|
||||||
const mockCommunityList = [];
|
|
||||||
const communityDataServiceStub = {
|
|
||||||
findAll: () => Observable.of(mockCommunityList),
|
|
||||||
findById: () => Observable.of(new Community())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class RouterStub {
|
|
||||||
navigateByUrl(url: string) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
||||||
@@ -89,6 +84,14 @@ describe('SearchPageComponent', () => {
|
|||||||
provide: SearchSidebarService,
|
provide: SearchSidebarService,
|
||||||
useValue: sidebarService
|
useValue: sidebarService
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SearchFilterService,
|
||||||
|
useValue: jasmine.createSpyObj('SearchFilterService', {
|
||||||
|
getPaginatedSearchOptions: hot('a', {
|
||||||
|
a: paginatedSearchOptions
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchPageComponent, {
|
}).overrideComponent(SearchPageComponent, {
|
||||||
@@ -103,54 +106,10 @@ describe('SearchPageComponent', () => {
|
|||||||
searchServiceObject = (comp as any).service;
|
searchServiceObject = (comp as any).service;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the scope and query based on the route parameters', () => {
|
it('should get the scope and query from the route parameters', () => {
|
||||||
expect(comp.query).toBe(queryParam);
|
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||||
expect((comp as any).scope).toBe(scopeParam);
|
b: paginatedSearchOptions
|
||||||
});
|
}));
|
||||||
|
|
||||||
describe('when update search results is called', () => {
|
|
||||||
let paginationUpdate;
|
|
||||||
let sortUpdate;
|
|
||||||
beforeEach(() => {
|
|
||||||
paginationUpdate = Object.assign(
|
|
||||||
{},
|
|
||||||
new PaginationComponentOptions(),
|
|
||||||
{
|
|
||||||
currentPage: 5,
|
|
||||||
pageSize: 15
|
|
||||||
}
|
|
||||||
);
|
|
||||||
sortUpdate = Object.assign({},
|
|
||||||
new SortOptions(),
|
|
||||||
{
|
|
||||||
direction: SortDirection.Ascending,
|
|
||||||
field: 'test-field'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the search function of the search service with the right parameters', () => {
|
|
||||||
spyOn(searchServiceObject, 'search').and.callThrough();
|
|
||||||
|
|
||||||
(comp as any).updateSearchResults({
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(searchServiceObject.search).toHaveBeenCalledWith(queryParam, scopeParam, {
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update the results', () => {
|
|
||||||
spyOn(searchServiceObject, 'search').and.callThrough();
|
|
||||||
|
|
||||||
(comp as any).updateSearchResults({});
|
|
||||||
|
|
||||||
expect(comp.resultsRDObs as any).toBe(mockResults);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the closeSidebar event is emitted clicked in mobile view', () => {
|
describe('when the closeSidebar event is emitted clicked in mobile view', () => {
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { flatMap, } from 'rxjs/operators';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { isNotEmpty } from '../shared/empty.util';
|
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
import { SearchOptions, ViewMode } from './search-options.model';
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
import { SearchResult } from './search-result.model';
|
import { SearchResult } from './search-result.model';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
@@ -29,96 +28,43 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [pushInOut]
|
animations: [pushInOut]
|
||||||
})
|
})
|
||||||
export class SearchPageComponent implements OnInit, OnDestroy {
|
export class SearchPageComponent implements OnInit {
|
||||||
|
|
||||||
private sub;
|
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
|
||||||
private scope: string;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
query: string;
|
|
||||||
scopeObjectRDObs: Observable<RemoteData<DSpaceObject>>;
|
|
||||||
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>>>>;
|
|
||||||
currentParams = {};
|
|
||||||
searchOptions: SearchOptions;
|
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
scopeListRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
scopeListRD$: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
isMobileView: Observable<boolean>;
|
isMobileView$: Observable<boolean>;
|
||||||
|
pageSize;
|
||||||
|
pageSizeOptions;
|
||||||
|
defaults = {
|
||||||
|
pagination: {
|
||||||
|
id: 'search-results-pagination',
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
sort: new SortOptions('score', SortDirection.DESC),
|
||||||
|
query: '',
|
||||||
|
scope: ''
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private communityService: CommunityDataService,
|
private communityService: CommunityDataService,
|
||||||
private sidebarService: SearchSidebarService,
|
private sidebarService: SearchSidebarService,
|
||||||
private windowService: HostWindowService) {
|
private windowService: HostWindowService,
|
||||||
this.isMobileView = Observable.combineLatest(
|
private filterService: SearchFilterService) {
|
||||||
|
this.isMobileView$ = Observable.combineLatest(
|
||||||
this.windowService.isXs(),
|
this.windowService.isXs(),
|
||||||
this.windowService.isSm(),
|
this.windowService.isSm(),
|
||||||
((isXs, isSm) => isXs || isSm)
|
((isXs, isSm) => isXs || isSm)
|
||||||
);
|
);
|
||||||
this.scopeListRDObs = communityService.findAll();
|
this.scopeListRD$ = communityService.findAll();
|
||||||
// Initial pagination config
|
|
||||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
|
||||||
pagination.id = 'search-results-pagination';
|
|
||||||
pagination.currentPage = 1;
|
|
||||||
pagination.pageSize = 10;
|
|
||||||
|
|
||||||
const sort: SortOptions = new SortOptions();
|
|
||||||
this.sortConfig = sort;
|
|
||||||
this.searchOptions = this.service.searchOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.sub = this.route
|
this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults);
|
||||||
.queryParams
|
this.resultsRD$ = this.searchOptions$.pipe(
|
||||||
.subscribe((params) => {
|
flatMap((searchOptions) => this.service.search(searchOptions))
|
||||||
// Save current parameters
|
);
|
||||||
this.currentParams = params;
|
|
||||||
this.query = params.query || '';
|
|
||||||
this.scope = params.scope;
|
|
||||||
const page = +params.page || this.searchOptions.pagination.currentPage;
|
|
||||||
let pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
|
||||||
let pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
|
|
||||||
|
|
||||||
if (isNotEmpty(params.view) && params.view === ViewMode.Grid) {
|
|
||||||
pageSizeOptions = [12, 24, 36, 48 , 50, 62, 74, 84];
|
|
||||||
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
|
||||||
pageSize = 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isNotEmpty(params.view) && params.view === ViewMode.List) {
|
|
||||||
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
|
||||||
pageSize = 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortDirection = +params.sortDirection || this.searchOptions.sort.direction;
|
|
||||||
const pagination = Object.assign({},
|
|
||||||
this.searchOptions.pagination,
|
|
||||||
{ currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions}
|
|
||||||
);
|
|
||||||
const sort = Object.assign({},
|
|
||||||
this.searchOptions.sort,
|
|
||||||
{ direction: sortDirection, field: params.sortField }
|
|
||||||
);
|
|
||||||
|
|
||||||
this.updateSearchResults({
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
if (isNotEmpty(this.scope)) {
|
|
||||||
this.scopeObjectRDObs = this.communityService.findById(this.scope);
|
|
||||||
} else {
|
|
||||||
this.scopeObjectRDObs = Observable.of(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateSearchResults(searchOptions) {
|
|
||||||
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
|
||||||
this.searchOptions = searchOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.sub.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeSidebar(): void {
|
public closeSidebar(): void {
|
||||||
@@ -132,4 +78,8 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
public isSidebarCollapsed(): Observable<boolean> {
|
public isSidebarCollapsed(): Observable<boolean> {
|
||||||
return this.sidebarService.isCollapsed;
|
return this.sidebarService.isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSearchLink(): string {
|
||||||
|
return this.service.getSearchLink();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CoreModule } from '../core/core.module';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
@@ -31,6 +32,7 @@ const effects = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
EffectsModule.forFeature(effects),
|
EffectsModule.forFeature(effects),
|
||||||
|
CoreModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
|
<h2>{{ 'search.results.head' | translate }}</h2>
|
||||||
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="searchConfig.pagination"
|
[config]="searchConfig.pagination"
|
||||||
[sortConfig]="searchConfig.sort"
|
[sortConfig]="searchConfig.sort"
|
||||||
[objects]="searchResults"
|
[objects]="searchResults"
|
||||||
[hideGear]="true">
|
[hideGear]="true">
|
||||||
</ds-viewable-collection></div>
|
</ds-viewable-collection></div>
|
||||||
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
<ds-loading *ngIf="!searchResults || searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
<ds-error *ngIf="searchResults?.payload?.page.length == 0" message="{{'search.results.no-results' | translate}}"></ds-error>
|
||||||
|
@@ -5,6 +5,7 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
|||||||
import { SearchOptions, ViewMode } from '../search-options.model';
|
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -20,7 +21,7 @@ import { SearchResult } from '../search-result.model';
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchResultsComponent {
|
export class SearchResultsComponent {
|
||||||
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
@Input() searchResults: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>;
|
||||||
@Input() searchConfig: SearchOptions;
|
@Input() searchConfig: SearchOptions;
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
@Input() viewMode: ViewMode;
|
@Input() viewMode: ViewMode;
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
|
|
||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
export class FacetValue {
|
export class FacetValue {
|
||||||
|
@autoserializeAs(String, 'label')
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
count: number;
|
count: number;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
search: string;
|
search: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export enum FilterType {
|
export enum FilterType {
|
||||||
text,
|
text,
|
||||||
range,
|
date,
|
||||||
hierarchy
|
hierarchical,
|
||||||
|
standard
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,27 @@
|
|||||||
import { FilterType } from './filter-type.model';
|
import { FilterType } from './filter-type.model';
|
||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
export class SearchFilterConfig {
|
export class SearchFilterConfig {
|
||||||
|
|
||||||
name: string;
|
@autoserialize
|
||||||
type: FilterType;
|
name: string;
|
||||||
hasFacets: boolean;
|
|
||||||
pageSize = 5;
|
@autoserializeAs(String, 'facetType')
|
||||||
isOpenByDefault: boolean;
|
type: FilterType;
|
||||||
/**
|
|
||||||
* Name of this configuration that can be used in a url
|
@autoserialize
|
||||||
* @returns Parameter name
|
hasFacets: boolean;
|
||||||
*/
|
|
||||||
get paramName(): string {
|
// @autoserializeAs(String, 'facetLimit') - uncomment when fixed in rest
|
||||||
return 'f.' + this.name;
|
pageSize = 5;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
isOpenByDefault: boolean;
|
||||||
|
/**
|
||||||
|
* Name of this configuration that can be used in a url
|
||||||
|
* @returns Parameter name
|
||||||
|
*/
|
||||||
|
get paramName(): string {
|
||||||
|
return 'f.' + this.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
|
|
||||||
|
export class SearchQueryResponse {
|
||||||
|
@autoserialize
|
||||||
|
scope: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
query: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
appliedFilters: any[]; // TODO
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
sort: any; // TODO
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
configurationName: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public type: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
page: PageInfo;
|
||||||
|
|
||||||
|
@autoserializeAs(NormalizedSearchResult)
|
||||||
|
objects: NormalizedSearchResult[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
facets: any; // TODO
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
next: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
previous: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
first: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
last: string;
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
|
const searchResultMap = new Map();
|
||||||
|
|
||||||
|
export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
|
||||||
|
return function decorator(searchResult: any) {
|
||||||
|
if (!searchResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchResultMap.set(domainConstructor, searchResult);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
|
||||||
|
return searchResultMap.get(domainConstructor);
|
||||||
|
}
|
@@ -8,50 +8,242 @@ import { SearchService } from './search.service';
|
|||||||
import { ItemDataService } from './../../core/data/item-data.service';
|
import { ItemDataService } from './../../core/data/item-data.service';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
import { RouteService } from '../../shared/route.service';
|
import { RouteService } from '../../shared/route.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { SearchResult } from '../search-result.model';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
||||||
|
import {
|
||||||
|
FacetConfigSuccessResponse, RestResponse,
|
||||||
|
SearchSuccessResponse
|
||||||
|
} from '../../core/cache/response-cache.models';
|
||||||
|
import { SearchQueryResponse } from './search-query-response.model';
|
||||||
|
import { SearchFilterConfig } from './search-filter-config.model';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent { }
|
class DummyComponent {
|
||||||
|
}
|
||||||
|
|
||||||
describe('SearchService', () => {
|
describe('SearchService', () => {
|
||||||
let searchService: SearchService;
|
describe('By default', () => {
|
||||||
|
let searchService: SearchService;
|
||||||
beforeEach(() => {
|
const router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
const route = new ActivatedRouteStub();
|
||||||
imports: [
|
beforeEach(() => {
|
||||||
CommonModule,
|
TestBed.configureTestingModule({
|
||||||
RouterTestingModule.withRoutes([
|
imports: [
|
||||||
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
|
CommonModule,
|
||||||
])
|
RouterTestingModule.withRoutes([
|
||||||
],
|
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
|
||||||
declarations: [
|
])
|
||||||
DummyComponent
|
],
|
||||||
],
|
declarations: [
|
||||||
providers: [
|
DummyComponent
|
||||||
{ provide: ItemDataService, useValue: {} },
|
],
|
||||||
{ provide: RouteService, useValue: {} },
|
providers: [
|
||||||
SearchService
|
{ provide: Router, useValue: router },
|
||||||
],
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
|
SearchService
|
||||||
|
],
|
||||||
|
});
|
||||||
|
searchService = TestBed.get(SearchService);
|
||||||
});
|
});
|
||||||
searchService = TestBed.get(SearchService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return list view mode by default', () => {
|
it('should return list view mode', () => {
|
||||||
searchService.getViewMode().subscribe((viewMode) => {
|
searchService.getViewMode().subscribe((viewMode) => {
|
||||||
expect(viewMode).toBe(ViewMode.List);
|
expect(viewMode).toBe(ViewMode.List);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('', () => {
|
||||||
|
let searchService: SearchService;
|
||||||
|
const router = new RouterStub();
|
||||||
|
const route = new ActivatedRouteStub();
|
||||||
|
|
||||||
it('should return the view mode set through setViewMode', fakeAsync(() => {
|
const halService = {
|
||||||
searchService.setViewMode(ViewMode.Grid)
|
/* tslint:disable:no-empty */
|
||||||
tick();
|
getEndpoint: () => {
|
||||||
let viewMode = ViewMode.List;
|
}
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
/* tslint:enable:no-empty */
|
||||||
expect(viewMode).toBe(ViewMode.Grid);
|
|
||||||
|
|
||||||
searchService.setViewMode(ViewMode.List)
|
};
|
||||||
tick();
|
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
|
||||||
expect(viewMode).toBe(ViewMode.List);
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
const remoteDataBuildService = {
|
||||||
|
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
|
||||||
|
return Observable.combineLatest(requestEntryObs,
|
||||||
|
responseCacheObs, payloadObs, (req, res, pay) => {
|
||||||
|
return { req, res, pay };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => {
|
||||||
|
return Observable.of(new RemoteData(false, false, true, null, []));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterTestingModule.withRoutes([
|
||||||
|
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
|
||||||
|
])
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
DummyComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||||
|
{ provide: HALEndpointService, useValue: halService },
|
||||||
|
SearchService
|
||||||
|
],
|
||||||
|
});
|
||||||
|
searchService = TestBed.get(SearchService);
|
||||||
|
const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } });
|
||||||
|
router.parseUrl.and.returnValue(urlTree);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
||||||
|
searchService.setViewMode(ViewMode.List);
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
|
queryParams: { view: ViewMode.List },
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
||||||
|
searchService.setViewMode(ViewMode.Grid);
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
|
queryParams: { view: ViewMode.Grid },
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
||||||
|
let viewMode = ViewMode.Grid;
|
||||||
|
route.testParams = { view: ViewMode.List };
|
||||||
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
|
expect(viewMode).toEqual(ViewMode.List);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
|
||||||
|
let viewMode = ViewMode.List;
|
||||||
|
route.testParams = { view: ViewMode.Grid };
|
||||||
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
|
expect(viewMode).toEqual(ViewMode.Grid);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when search is called', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/test';
|
||||||
|
const searchOptions = new PaginatedSearchOptions();
|
||||||
|
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
||||||
|
const response = new SearchSuccessResponse(queryResponse, '200');
|
||||||
|
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
|
||||||
|
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.search(searchOptions).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.configure).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
|
});
|
||||||
|
it('should call get on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getConfig is called without a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
|
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
|
||||||
|
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getConfig(null).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.configure).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
|
});
|
||||||
|
it('should call get on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getConfig is called with a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
const scope = 'test';
|
||||||
|
const requestUrl = endPoint + '?scope=' + scope;
|
||||||
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
|
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
|
||||||
|
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getConfig(scope).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.configure).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
|
||||||
|
});
|
||||||
|
it('should call get on the request service with the correct request url', () => {
|
||||||
|
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,218 +1,220 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
import {
|
||||||
|
ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router,
|
||||||
|
UrlSegmentGroup
|
||||||
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { flatMap, map, tap } from 'rxjs/operators';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import {
|
||||||
|
FacetConfigSuccessResponse,
|
||||||
|
FacetValueSuccessResponse,
|
||||||
|
SearchSuccessResponse
|
||||||
|
} from '../../core/cache/response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { ResponseParsingService } from '../../core/data/parsing.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { GetRequest, RestRequest } from '../../core/data/request.models';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { Metadatum } from '../../core/shared/metadatum.model';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model';
|
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { RouteService } from '../../shared/route.service';
|
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
import { FacetValue } from './facet-value.model';
|
import { FacetValue } from './facet-value.model';
|
||||||
import { FilterType } from './filter-type.model';
|
|
||||||
import { SearchFilterConfig } from './search-filter-config.model';
|
import { SearchFilterConfig } from './search-filter-config.model';
|
||||||
|
import { SearchResponseParsingService } from '../../core/data/search-response-parsing.service';
|
||||||
function shuffle(array: any[]) {
|
import { SearchQueryResponse } from './search-query-response.model';
|
||||||
let i = 0;
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
let j = 0;
|
import { getSearchResultFor } from './search-result-element-decorator';
|
||||||
let temp = null;
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service';
|
||||||
for (i = array.length - 1; i > 0; i -= 1) {
|
import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service';
|
||||||
j = Math.floor(Math.random() * (i + 1));
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
temp = array[i];
|
import { observable } from 'rxjs/symbol/observable';
|
||||||
array[i] = array[j];
|
|
||||||
array[j] = temp;
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService implements OnDestroy {
|
export class SearchService implements OnDestroy {
|
||||||
|
private searchLinkPath = 'discover/search/objects';
|
||||||
|
private facetValueLinkPathPrefix = 'discover/facets/';
|
||||||
|
private facetConfigLinkPath = 'discover/facets';
|
||||||
|
|
||||||
totalPages = 5;
|
|
||||||
mockedHighlights: string[] = new Array(
|
|
||||||
'This is a <em>sample abstract</em>.',
|
|
||||||
'This is a sample abstract. But, to fill up some space, here\'s <em>"Hello"</em> in several different languages : ',
|
|
||||||
'This is a Sample HTML webpage including several <em>images</em> and styles (CSS).',
|
|
||||||
'This is <em>really</em> just a sample abstract. But, Í’vé thrown ïn a cõuple of spëciâl charactèrs för êxtrå fuñ!',
|
|
||||||
'This abstract is <em>really quite great</em>',
|
|
||||||
'The solution structure of the <em>bee</em> venom neurotoxin',
|
|
||||||
'BACKGROUND: The <em>Open Archive Initiative (OAI)</em> refers to a movement started around the \'90 s to guarantee free access to scientific information',
|
|
||||||
'The collision fault detection of a <em>XXY</em> stage is proposed for the first time in this paper',
|
|
||||||
'<em>This was blank in the actual item, no abstract</em>',
|
|
||||||
'<em>The QSAR DataBank (QsarDB) repository</em>',
|
|
||||||
);
|
|
||||||
private sub;
|
private sub;
|
||||||
searchLink = '/search';
|
|
||||||
|
|
||||||
config: SearchFilterConfig[] = [
|
|
||||||
Object.assign(new SearchFilterConfig(),
|
|
||||||
{
|
|
||||||
name: 'scope',
|
|
||||||
type: FilterType.hierarchy,
|
|
||||||
hasFacets: true,
|
|
||||||
isOpenByDefault: true
|
|
||||||
}),
|
|
||||||
Object.assign(new SearchFilterConfig(),
|
|
||||||
{
|
|
||||||
name: 'author',
|
|
||||||
type: FilterType.text,
|
|
||||||
hasFacets: true,
|
|
||||||
isOpenByDefault: false
|
|
||||||
}),
|
|
||||||
Object.assign(new SearchFilterConfig(),
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
type: FilterType.range,
|
|
||||||
hasFacets: true,
|
|
||||||
isOpenByDefault: false
|
|
||||||
}),
|
|
||||||
Object.assign(new SearchFilterConfig(),
|
|
||||||
{
|
|
||||||
name: 'subject',
|
|
||||||
type: FilterType.text,
|
|
||||||
hasFacets: false,
|
|
||||||
isOpenByDefault: false
|
|
||||||
})
|
|
||||||
];
|
|
||||||
// searchOptions: BehaviorSubject<SearchOptions>;
|
|
||||||
searchOptions: SearchOptions;
|
searchOptions: SearchOptions;
|
||||||
|
|
||||||
constructor(private itemDataService: ItemDataService,
|
constructor(private router: Router,
|
||||||
private routeService: RouteService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router) {
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
private rdb: RemoteDataBuildService,
|
||||||
|
private halService: HALEndpointService) {
|
||||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
const sort: SortOptions = new SortOptions();
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
this.searchOptions = { pagination: pagination, sort: sort };
|
this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort });
|
||||||
// this.searchOptions = new BehaviorSubject<SearchOptions>(searchOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
this.searchOptions = searchOptions;
|
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||||
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
map((url: string) => {
|
||||||
if (hasValue(scopeId)) {
|
if (hasValue(searchOptions)) {
|
||||||
self += `&scope=${scopeId}`;
|
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.currentPage)) {
|
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||||
self += `&page=${searchOptions.pagination.currentPage}`;
|
return Object.assign(request, {
|
||||||
}
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.pageSize)) {
|
return SearchResponseParsingService;
|
||||||
self += `&pageSize=${searchOptions.pagination.pageSize}`;
|
}
|
||||||
}
|
});
|
||||||
if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.direction)) {
|
}),
|
||||||
self += `&sortDirection=${searchOptions.sort.direction}`;
|
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||||
}
|
);
|
||||||
if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.field)) {
|
const requestEntryObs = requestObs.pipe(
|
||||||
self += `&sortField=${searchOptions.sort.field}`;
|
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
}
|
);
|
||||||
|
|
||||||
const error = undefined;
|
const responseCacheObs = requestObs.pipe(
|
||||||
const returningPageInfo = new PageInfo();
|
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||||
|
);
|
||||||
|
|
||||||
if (isNotEmpty(searchOptions)) {
|
// get search results from response cache
|
||||||
returningPageInfo.elementsPerPage = searchOptions.pagination.pageSize;
|
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe(
|
||||||
returningPageInfo.currentPage = searchOptions.pagination.currentPage;
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
} else {
|
map((response: SearchSuccessResponse) => response.results)
|
||||||
returningPageInfo.elementsPerPage = 10;
|
);
|
||||||
returningPageInfo.currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsObs = this.itemDataService.findAll({
|
// turn dspace href from search results to effective list of DSpaceObjects
|
||||||
scopeID: scopeId,
|
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
||||||
currentPage: returningPageInfo.currentPage,
|
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
||||||
elementsPerPage: returningPageInfo.elementsPerPage
|
map((sqr: SearchQueryResponse) => {
|
||||||
|
return sqr.objects.map((nsr: NormalizedSearchResult) =>
|
||||||
|
this.rdb.buildSingle(nsr.dspaceObject));
|
||||||
|
}),
|
||||||
|
flatMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create search results again with the correct dso objects linked to each result
|
||||||
|
const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData<DSpaceObject[]>) => {
|
||||||
|
|
||||||
|
return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
|
||||||
|
let co = DSpaceObject;
|
||||||
|
if (dsos.payload[index]) {
|
||||||
|
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
|
||||||
|
co = getSearchResultFor(constructor);
|
||||||
|
return Object.assign(new co(), object, {
|
||||||
|
dspaceObject: dsos.payload[index]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return itemsObs
|
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||||
.filter((rd: RemoteData<PaginatedList<Item>>) => rd.hasSucceeded)
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
.map((rd: RemoteData<PaginatedList<Item>>) => {
|
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||||
|
);
|
||||||
|
|
||||||
const totalElements = rd.payload.totalElements > 20 ? 20 : rd.payload.totalElements;
|
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
|
||||||
|
return new PaginatedList(pageInfo, tDomainList);
|
||||||
|
});
|
||||||
|
|
||||||
const page = shuffle(rd.payload.page)
|
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||||
.map((item: Item, index: number) => {
|
|
||||||
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
|
||||||
mockResult.dspaceObject = item;
|
|
||||||
const highlight = new Metadatum();
|
|
||||||
highlight.key = 'dc.description.abstract';
|
|
||||||
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
|
|
||||||
mockResult.hitHighlights = new Array(highlight);
|
|
||||||
return mockResult;
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = Object.assign({}, rd.payload, { totalElements: totalElements, page });
|
|
||||||
|
|
||||||
return new RemoteData(
|
|
||||||
rd.isRequestPending,
|
|
||||||
rd.isResponsePending,
|
|
||||||
rd.hasSucceeded,
|
|
||||||
error,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
}).startWith(new RemoteData(
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig(): Observable<RemoteData<SearchFilterConfig[]>> {
|
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||||
const requestPending = false;
|
const requestObs = this.halService.getEndpoint(this.facetConfigLinkPath).pipe(
|
||||||
const responsePending = false;
|
map((url: string) => {
|
||||||
const isSuccessful = true;
|
const args: string[] = [];
|
||||||
const error = undefined;
|
|
||||||
return Observable.of(new RemoteData(
|
|
||||||
requestPending,
|
|
||||||
responsePending,
|
|
||||||
isSuccessful,
|
|
||||||
error,
|
|
||||||
this.config
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
getFacetValuesFor(searchFilterConfigName: string): Observable<RemoteData<FacetValue[]>> {
|
if (isNotEmpty(scope)) {
|
||||||
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
|
args.push(`scope=${scope}`);
|
||||||
return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
|
|
||||||
const payload: FacetValue[] = [];
|
|
||||||
const totalFilters = 13;
|
|
||||||
for (let i = 0; i < totalFilters; i++) {
|
|
||||||
const value = searchFilterConfigName + ' ' + (i + 1);
|
|
||||||
if (!selectedValues.includes(value)) {
|
|
||||||
payload.push({
|
|
||||||
value: value,
|
|
||||||
count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count
|
|
||||||
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const requestPending = false;
|
|
||||||
const responsePending = false;
|
if (isNotEmpty(args)) {
|
||||||
const isSuccessful = true;
|
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||||
const error = undefined;
|
}
|
||||||
return new RemoteData(
|
|
||||||
requestPending,
|
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||||
responsePending,
|
return Object.assign(request, {
|
||||||
isSuccessful,
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
error,
|
return FacetConfigResponseParsingService;
|
||||||
payload
|
}
|
||||||
)
|
});
|
||||||
}
|
}),
|
||||||
)
|
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestEntryObs = requestObs.pipe(
|
||||||
|
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseCacheObs = requestObs.pipe(
|
||||||
|
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||||
|
);
|
||||||
|
|
||||||
|
// get search results from response cache
|
||||||
|
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: FacetConfigSuccessResponse) =>
|
||||||
|
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable<RemoteData<PaginatedList<FacetValue>>> {
|
||||||
|
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
|
||||||
|
map((url: string) => {
|
||||||
|
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
||||||
|
if (hasValue(searchOptions)) {
|
||||||
|
url = searchOptions.toRestUrl(url, args);
|
||||||
|
}
|
||||||
|
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||||
|
return Object.assign(request, {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return FacetValueResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestEntryObs = requestObs.pipe(
|
||||||
|
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseCacheObs = requestObs.pipe(
|
||||||
|
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||||
|
);
|
||||||
|
|
||||||
|
// get search results from response cache
|
||||||
|
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: FacetValueSuccessResponse) => response.results)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||||
|
map((entry: ResponseCacheEntry) => entry.response),
|
||||||
|
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||||
|
);
|
||||||
|
|
||||||
|
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
|
||||||
|
return new PaginatedList(pageInfo, facetValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewMode(): Observable<ViewMode> {
|
getViewMode(): Observable<ViewMode> {
|
||||||
@@ -231,25 +233,13 @@ export class SearchService implements OnDestroy {
|
|||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.router.navigate([this.searchLink], navigationExtras);
|
this.router.navigate([this.getSearchLink()], navigationExtras);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClearFiltersQueryParams(): any {
|
getSearchLink(): string {
|
||||||
const params = {};
|
const urlTree = this.router.parseUrl(this.router.url);
|
||||||
this.sub = this.route.queryParamMap
|
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||||
.subscribe((map) => {
|
return '/' + g.toString();
|
||||||
map.keys
|
|
||||||
.filter((key) => this.config
|
|
||||||
.findIndex((conf: SearchFilterConfig) => conf.paramName === key) < 0)
|
|
||||||
.forEach((key) => {
|
|
||||||
params[key] = map.get(key);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchLink() {
|
|
||||||
return this.searchLink;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@@ -2,21 +2,21 @@
|
|||||||
<div *ngIf="[searchOptions].sort" class="setting-option result-order-settings mb-3 p-3">
|
<div *ngIf="[searchOptions].sort" class="setting-option result-order-settings mb-3 p-3">
|
||||||
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
|
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
|
||||||
<select class="form-control" (change)="reloadOrder($event)">
|
<select class="form-control" (change)="reloadOrder($event)">
|
||||||
<option *ngFor="let direction of (sortDirections | dsKeys); let currentElementIndex = index"
|
<option *ngFor="let sortDirection of (sortDirections | dsKeys)"
|
||||||
[value]="currentElementIndex"
|
[value]="sortDirection.value"
|
||||||
[selected]="direction === searchOptions.sort? 'selected': null">
|
[selected]="sortDirection.value === direction? 'selected': null">
|
||||||
{{direction.value}}
|
{{'sorting.' + sortDirection.key | translate}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="searchOptions.pagination.pageSize" class="setting-option page-size-settings mb-3 p-3">
|
<div class="setting-option page-size-settings mb-3 p-3">
|
||||||
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
||||||
|
|
||||||
<select class="form-control" (change)="reloadRPP($event)">
|
<select class="form-control" (change)="reloadRPP($event)">
|
||||||
<option *ngFor="let item of pageSizeOptions" [value]="item"
|
<option *ngFor="let pageSizeOption of pageSizeOptions" [value]="pageSizeOption"
|
||||||
[selected]="item === searchOptions.pagination.pageSize ? 'selected': null">
|
[selected]="pageSizeOption === pageSize ? 'selected': null">
|
||||||
{{item}}
|
{{pageSizeOption}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { SearchSettingsComponent } from './search-settings.component';
|
import { SearchSettingsComponent } from './search-settings.component';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@@ -22,7 +22,7 @@ describe('SearchSettingsComponent', () => {
|
|||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
const sort: SortOptions = new SortOptions();
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = [ 'test', 'data' ];
|
const mockResults = [ 'test', 'data' ];
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
searchOptions: { pagination: pagination, sort: sort },
|
searchOptions: { pagination: pagination, sort: sort },
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { SearchOptions, ViewMode } from '../search-options.model';
|
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||||
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-settings',
|
selector: 'ds-search-settings',
|
||||||
styleUrls: ['./search-settings.component.scss'],
|
styleUrls: ['./search-settings.component.scss'],
|
||||||
templateUrl: './search-settings.component.html',
|
templateUrl: './search-settings.component.html'
|
||||||
})
|
})
|
||||||
export class SearchSettingsComponent implements OnInit {
|
export class SearchSettingsComponent implements OnInit {
|
||||||
|
|
||||||
@Input() searchOptions: SearchOptions;
|
@Input() searchOptions: PaginatedSearchOptions;
|
||||||
/**
|
/**
|
||||||
* Declare SortDirection enumeration to use it in the template
|
* Declare SortDirection enumeration to use it in the template
|
||||||
*/
|
*/
|
||||||
@@ -21,8 +22,6 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public pageSize;
|
public pageSize;
|
||||||
@Input() public pageSizeOptions;
|
@Input() public pageSizeOptions;
|
||||||
public listPageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
|
|
||||||
public gridPageSizeOptions: number[] = [12, 24, 36, 48 , 50, 62, 74, 84];
|
|
||||||
|
|
||||||
private sub;
|
private sub;
|
||||||
private scope: string;
|
private scope: string;
|
||||||
@@ -48,11 +47,11 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
this.scope = params.scope;
|
this.scope = params.scope;
|
||||||
this.page = +params.page || this.searchOptions.pagination.currentPage;
|
this.page = +params.page || this.searchOptions.pagination.currentPage;
|
||||||
this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
||||||
this.direction = +params.sortDirection || this.searchOptions.sort.direction;
|
this.direction = params.sortDirection || this.searchOptions.sort.direction;
|
||||||
if (params.view === ViewMode.Grid) {
|
if (params.view === ViewMode.Grid) {
|
||||||
this.pageSizeOptions = this.gridPageSizeOptions;
|
this.pageSizeOptions = this.pageSizeOptions;
|
||||||
} else {
|
} else {
|
||||||
this.pageSizeOptions = this.listPageSizeOptions;
|
this.pageSizeOptions = this.pageSizeOptions;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,8 @@ import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
|||||||
|
|
||||||
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
|
||||||
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
|
||||||
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -56,6 +58,7 @@ describe('App component', () => {
|
|||||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||||
|
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@@ -10,6 +10,7 @@ import { MetadataService } from './core/metadata/metadata.service';
|
|||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
import { HostWindowState } from './shared/host-window.reducer';
|
import { HostWindowState } from './shared/host-window.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
||||||
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -20,11 +21,14 @@ import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
|
|
||||||
constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
private translate: TranslateService,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
private store: Store<HostWindowState>,
|
private translate: TranslateService,
|
||||||
private metadata: MetadataService) {
|
private store: Store<HostWindowState>,
|
||||||
|
private metadata: MetadataService,
|
||||||
|
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics
|
||||||
|
) {
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
translate.setDefaultLang('en');
|
translate.setDefaultLang('en');
|
||||||
// the lang to use, if the lang isn't available, it will use the current loader to get them
|
// the lang to use, if the lang isn't available, it will use the current loader to get them
|
||||||
|
@@ -3,11 +3,11 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca
|
|||||||
import { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
import { hot, cold, getTestScheduler } from 'jasmine-marbles';
|
import { hot, cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { BrowseEndpointRequest } from '../data/request.models';
|
import { BrowseEndpointRequest } from '../data/request.models';
|
||||||
import { TestScheduler } from 'rxjs/Rx';
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -15,8 +15,8 @@ describe('BrowseService', () => {
|
|||||||
let responseCache: ResponseCacheService;
|
let responseCache: ResponseCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
|
||||||
const envConfig = {} as GlobalConfig;
|
|
||||||
const browsesEndpointURL = 'https://rest.api/browses';
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
|
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||||
const browseDefinitions = [
|
const browseDefinitions = [
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
metadataBrowse: false,
|
metadataBrowse: false,
|
||||||
@@ -91,7 +91,7 @@ describe('BrowseService', () => {
|
|||||||
return new BrowseService(
|
return new BrowseService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
envConfig
|
halService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +106,16 @@ describe('BrowseService', () => {
|
|||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the URL for the given metadatumKey and linkName', () => {
|
it('should return the URL for the given metadatumKey and linkPath', () => {
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
const expectedURL = browseDefinitions[0]._links[linkName];
|
const expectedURL = browseDefinitions[0]._links[linkPath];
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
@@ -123,10 +123,10 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
it('should work when the definition uses a wildcard in the metadatumKey', () => {
|
it('should work when the definition uses a wildcard in the metadatumKey', () => {
|
||||||
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
const expectedURL = browseDefinitions[1]._links[linkName];
|
const expectedURL = browseDefinitions[1]._links[linkPath];
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
@@ -134,30 +134,30 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
it('should throw an error when the key doesn\'t match', () => {
|
it('should throw an error when the key doesn\'t match', () => {
|
||||||
const metadatumKey = 'dc.title'; // isn't in the definitions
|
const metadatumKey = 'dc.title'; // isn't in the definitions
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`));
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when the link doesn\'t match', () => {
|
it('should throw an error when the link doesn\'t match', () => {
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
const linkName = 'collections'; // isn't in the definitions
|
const linkPath = 'collections'; // isn't in the definitions
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`));
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure a new BrowseEndpointRequest', () => {
|
it('should configure a new BrowseEndpointRequest', () => {
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe());
|
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
@@ -171,13 +171,13 @@ describe('BrowseService', () => {
|
|||||||
responseCache = initMockResponseCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
.returnValue(hot('----'));
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('b---', { b: undefined });
|
const expected = cold('b---', { b: undefined });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -188,13 +188,13 @@ describe('BrowseService', () => {
|
|||||||
responseCache = initMockResponseCacheService(false);
|
responseCache = initMockResponseCacheService(false);
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
|
|
||||||
const metadatumKey = 'dc.date.issued';
|
const metadatumKey = 'dc.date.issued';
|
||||||
const linkName = 'items';
|
const linkPath = 'items';
|
||||||
|
|
||||||
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
const result = service.getBrowseURLFor(metadatumKey, linkPath);
|
||||||
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
|
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
@@ -12,8 +12,8 @@ import { BrowseDefinition } from '../shared/browse-definition.model';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseService extends HALEndpointService {
|
export class BrowseService {
|
||||||
protected linkName = 'browses';
|
protected linkPath = 'browses';
|
||||||
|
|
||||||
private static toSearchKeyArray(metadatumKey: string): string[] {
|
private static toSearchKeyArray(metadatumKey: string): string[] {
|
||||||
const keyParts = metadatumKey.split('.');
|
const keyParts = metadatumKey.split('.');
|
||||||
@@ -31,13 +31,12 @@ export class BrowseService extends HALEndpointService {
|
|||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURLFor(metadatumKey: string, linkName: string): Observable<string> {
|
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||||
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||||
return this.getEndpoint()
|
return this.halService.getEndpoint(this.linkPath)
|
||||||
.filter((href: string) => isNotEmpty(href))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
|
.map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
|
||||||
@@ -59,10 +58,10 @@ export class BrowseService extends HALEndpointService {
|
|||||||
return isNotEmpty(matchingKeys);
|
return isNotEmpty(matchingKeys);
|
||||||
})
|
})
|
||||||
).map((def: BrowseDefinition) => {
|
).map((def: BrowseDefinition) => {
|
||||||
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) {
|
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
|
||||||
throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`);
|
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
|
||||||
} else {
|
} else {
|
||||||
return def._links[linkName];
|
return def._links[linkPath];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -1,43 +1,44 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { NormalizedSearchResult } from '../../../+search-page/normalized-search-result.model';
|
||||||
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
|
import { SearchQueryResponse } from '../../../+search-page/search-service/search-query-response.model';
|
||||||
|
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { RemoteDataError } from '../../data/remote-data-error';
|
import { RemoteDataError } from '../../data/remote-data-error';
|
||||||
import { GetRequest } from '../../data/request.models';
|
import { GetRequest, RestRequest } from '../../data/request.models';
|
||||||
import { RequestEntry } from '../../data/request.reducer';
|
import { RequestEntry } from '../../data/request.reducer';
|
||||||
import { RequestService } from '../../data/request.service';
|
import { RequestService } from '../../data/request.service';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
|
import { NormalizedDSpaceObject } from '../models/normalized-dspace-object.model';
|
||||||
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
||||||
|
|
||||||
import { CacheableObject } from '../object-cache.reducer';
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
|
import { DSOSuccessResponse, ErrorResponse, SearchSuccessResponse } from '../response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../response-cache.reducer';
|
import { ResponseCacheEntry } from '../response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../response-cache.service';
|
import { ResponseCacheService } from '../response-cache.service';
|
||||||
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
||||||
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
constructor(
|
constructor(protected objectCache: ObjectCacheService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
protected requestService: RequestService) {
|
||||||
protected requestService: RequestService
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSingle<TNormalized extends CacheableObject, TDomain>(
|
buildSingle<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain>> {
|
||||||
hrefObs: string | Observable<string>,
|
|
||||||
normalizedType: GenericConstructor<TNormalized>
|
|
||||||
): Observable<RemoteData<TDomain>> {
|
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof hrefObs === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
hrefObs = Observable.of(hrefObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestHrefObs = hrefObs.flatMap((href: string) =>
|
const requestHrefObs = hrefObs.flatMap((href: string) =>
|
||||||
this.objectCache.getRequestHrefBySelfLink(href));
|
this.objectCache.getRequestHrefBySelfLink(href));
|
||||||
|
|
||||||
const requestObs = Observable.race(
|
const requestEntryObs = Observable.race(
|
||||||
hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
||||||
.filter((entry) => hasValue(entry)),
|
.filter((entry) => hasValue(entry)),
|
||||||
requestHrefObs.flatMap((requestHref) =>
|
requestHrefObs.flatMap((requestHref) =>
|
||||||
@@ -53,14 +54,14 @@ export class RemoteDataBuildService {
|
|||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payloadObs =
|
const payloadObs =
|
||||||
Observable.combineLatest(
|
Observable.combineLatest(
|
||||||
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href, normalizedType))
|
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href))
|
||||||
.startWith(undefined),
|
.startWith(undefined),
|
||||||
responseCacheObs
|
responseCacheObs
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
||||||
.flatMap((resourceSelfLinks: string[]) => {
|
.flatMap((resourceSelfLinks: string[]) => {
|
||||||
if (isNotEmpty(resourceSelfLinks)) {
|
if (isNotEmpty(resourceSelfLinks)) {
|
||||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0], normalizedType);
|
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||||
} else {
|
} else {
|
||||||
return Observable.of(undefined);
|
return Observable.of(undefined);
|
||||||
}
|
}
|
||||||
@@ -80,12 +81,12 @@ export class RemoteDataBuildService {
|
|||||||
})
|
})
|
||||||
.startWith(undefined)
|
.startWith(undefined)
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs);
|
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toRemoteDataObservable<T>(hrefObs: Observable<string>, requestObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
|
||||||
return Observable.combineLatest(hrefObs, requestObs, responseCacheObs.startWith(undefined), payloadObs,
|
return Observable.combineLatest(requestEntryObs, responseCacheObs.startWith(undefined), payloadObs,
|
||||||
(href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
||||||
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||||
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
let isSuccessful: boolean;
|
let isSuccessful: boolean;
|
||||||
@@ -93,7 +94,9 @@ export class RemoteDataBuildService {
|
|||||||
if (hasValue(resEntry) && hasValue(resEntry.response)) {
|
if (hasValue(resEntry) && hasValue(resEntry.response)) {
|
||||||
isSuccessful = resEntry.response.isSuccessful;
|
isSuccessful = resEntry.response.isSuccessful;
|
||||||
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
|
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
|
||||||
error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
|
if (hasValue(errorMessage)) {
|
||||||
|
error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
@@ -106,15 +109,12 @@ export class RemoteDataBuildService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildList<TNormalized extends CacheableObject, TDomain>(
|
buildList<TNormalized extends NormalizedObject, TDomain>(hrefObs: string | Observable<string>): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
||||||
hrefObs: string | Observable<string>,
|
|
||||||
normalizedType: GenericConstructor<TNormalized>
|
|
||||||
): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
|
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof hrefObs === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
hrefObs = Observable.of(hrefObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
const requestEntryObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
|
||||||
.filter((entry) => hasValue(entry));
|
.filter((entry) => hasValue(entry));
|
||||||
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
||||||
.filter((entry) => hasValue(entry));
|
.filter((entry) => hasValue(entry));
|
||||||
@@ -123,7 +123,7 @@ export class RemoteDataBuildService {
|
|||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
||||||
.flatMap((resourceUUIDs: string[]) => {
|
.flatMap((resourceUUIDs: string[]) => {
|
||||||
return this.objectCache.getList(resourceUUIDs, normalizedType)
|
return this.objectCache.getList(resourceUUIDs)
|
||||||
.map((normList: TNormalized[]) => {
|
.map((normList: TNormalized[]) => {
|
||||||
return normList.map((normalized: TNormalized) => {
|
return normList.map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
@@ -154,10 +154,10 @@ export class RemoteDataBuildService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs);
|
return this.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||||
const links: any = {};
|
const links: any = {};
|
||||||
|
|
||||||
const relationships = getRelationships(normalized.constructor) || [];
|
const relationships = getRelationships(normalized.constructor) || [];
|
||||||
@@ -165,7 +165,6 @@ export class RemoteDataBuildService {
|
|||||||
relationships.forEach((relationship: string) => {
|
relationships.forEach((relationship: string) => {
|
||||||
if (hasValue(normalized[relationship])) {
|
if (hasValue(normalized[relationship])) {
|
||||||
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
||||||
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
|
||||||
if (Array.isArray(normalized[relationship])) {
|
if (Array.isArray(normalized[relationship])) {
|
||||||
normalized[relationship].forEach((href: string) => {
|
normalized[relationship].forEach((href: string) => {
|
||||||
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href))
|
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href))
|
||||||
@@ -173,7 +172,7 @@ export class RemoteDataBuildService {
|
|||||||
|
|
||||||
const rdArr = [];
|
const rdArr = [];
|
||||||
normalized[relationship].forEach((href: string) => {
|
normalized[relationship].forEach((href: string) => {
|
||||||
rdArr.push(this.buildSingle(href, resourceConstructor));
|
rdArr.push(this.buildSingle(href));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isList) {
|
if (isList) {
|
||||||
@@ -188,9 +187,9 @@ export class RemoteDataBuildService {
|
|||||||
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
|
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
|
||||||
// but it should still be built as a list
|
// but it should still be built as a list
|
||||||
if (isList) {
|
if (isList) {
|
||||||
links[relationship] = this.buildList(normalized[relationship], resourceConstructor);
|
links[relationship] = this.buildList(normalized[relationship]);
|
||||||
} else {
|
} else {
|
||||||
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
|
links[relationship] = this.buildSingle(normalized[relationship]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +200,11 @@ export class RemoteDataBuildService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
|
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
|
||||||
|
|
||||||
|
if (isEmpty(input)) {
|
||||||
|
return Observable.of(new RemoteData(false, false, true, null, []));
|
||||||
|
}
|
||||||
|
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
...input,
|
...input,
|
||||||
(...arr: Array<RemoteData<T>>) => {
|
(...arr: Array<RemoteData<T>>) => {
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
import { inheritSerialization, autoserialize } from 'cerialize';
|
|
||||||
|
|
||||||
import { mapsTo } from '../builders/build-decorators';
|
|
||||||
|
|
||||||
import { BitstreamFormat } from '../../shared/bitstream-format.model';
|
|
||||||
import { NormalizedObject } from './normalized-object.model';
|
|
||||||
|
|
||||||
@mapsTo(BitstreamFormat)
|
|
||||||
@inheritSerialization(NormalizedObject)
|
|
||||||
export class NormalizedBitstreamFormat extends NormalizedObject {
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
shortDescription: string;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
mimetype: string;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
supportLevel: number;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
internal: boolean;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
extensions: string;
|
|
||||||
|
|
||||||
}
|
|
@@ -1,13 +1,16 @@
|
|||||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
import { Metadatum } from '../../shared/metadatum.model';
|
import { Metadatum } from '../../shared/metadatum.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { mapsTo } from '../builders/build-decorators';
|
||||||
import { NormalizedObject } from './normalized-object.model';
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a DSpaceObject.
|
* An model class for a DSpaceObject.
|
||||||
*/
|
*/
|
||||||
export abstract class NormalizedDSpaceObject extends NormalizedObject {
|
@mapsTo(DSpaceObject)
|
||||||
|
export class NormalizedDSpaceObject extends NormalizedObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The link to the rest endpoint where this object can be found
|
* The link to the rest endpoint where this object can be found
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
|
||||||
import { NormalizedBitstream } from './normalized-bitstream.model';
|
import { NormalizedBitstream } from './normalized-bitstream.model';
|
||||||
import { NormalizedBundle } from './normalized-bundle.model';
|
import { NormalizedBundle } from './normalized-bundle.model';
|
||||||
import { NormalizedItem } from './normalized-item.model';
|
import { NormalizedItem } from './normalized-item.model';
|
||||||
@@ -7,7 +6,6 @@ import { GenericConstructor } from '../../shared/generic-constructor';
|
|||||||
import { NormalizedCommunity } from './normalized-community.model';
|
import { NormalizedCommunity } from './normalized-community.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import { NormalizedObject } from './normalized-object.model';
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
|
|
||||||
|
|
||||||
export class NormalizedObjectFactory {
|
export class NormalizedObjectFactory {
|
||||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
||||||
@@ -15,9 +13,6 @@ export class NormalizedObjectFactory {
|
|||||||
case ResourceType.Bitstream: {
|
case ResourceType.Bitstream: {
|
||||||
return NormalizedBitstream
|
return NormalizedBitstream
|
||||||
}
|
}
|
||||||
case ResourceType.BitstreamFormat: {
|
|
||||||
return NormalizedBitstreamFormat
|
|
||||||
}
|
|
||||||
case ResourceType.Bundle: {
|
case ResourceType.Bundle: {
|
||||||
return NormalizedBundle
|
return NormalizedBundle
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { CacheableObject } from '../object-cache.reducer';
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize } from 'cerialize';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a NormalizedObject.
|
* An abstract model class for a NormalizedObject.
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +18,9 @@ export abstract class NormalizedObject implements CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
_links: {
|
_links: {
|
||||||
[name: string]: string
|
[name: string]: string
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
export enum SortDirection {
|
export enum SortDirection {
|
||||||
Ascending,
|
ASC = 'ASC',
|
||||||
Descending
|
DESC = 'DESC'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SortOptions {
|
export class SortOptions {
|
||||||
|
constructor(public field: string, public direction: SortDirection) {
|
||||||
constructor(public field: string = 'name', public direction: SortDirection = SortDirection.Ascending) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
src/app/core/cache/object-cache.reducer.ts
vendored
2
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -4,6 +4,7 @@ import {
|
|||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { CacheEntry } from './cache-entry';
|
import { CacheEntry } from './cache-entry';
|
||||||
|
import { ResourceType } from '../shared/resource-type';
|
||||||
|
|
||||||
export enum DirtyType {
|
export enum DirtyType {
|
||||||
Created = 'Created',
|
Created = 'Created',
|
||||||
@@ -19,6 +20,7 @@ export enum DirtyType {
|
|||||||
export interface CacheableObject {
|
export interface CacheableObject {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
self: string;
|
self: string;
|
||||||
|
type?: ResourceType;
|
||||||
// isNew: boolean;
|
// isNew: boolean;
|
||||||
// dirtyType: DirtyType;
|
// dirtyType: DirtyType;
|
||||||
// hasDirtyAttributes: boolean;
|
// hasDirtyAttributes: boolean;
|
||||||
|
46
src/app/core/cache/object-cache.service.spec.ts
vendored
46
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -2,20 +2,10 @@ import { Store } from '@ngrx/store';
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import { CacheableObject } from './object-cache.reducer';
|
|
||||||
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ResourceType } from '../shared/resource-type';
|
||||||
class TestClass implements CacheableObject {
|
import { NormalizedItem } from './models/normalized-item.model';
|
||||||
constructor(
|
|
||||||
public self: string,
|
|
||||||
public foo: string
|
|
||||||
) { }
|
|
||||||
|
|
||||||
test(): string {
|
|
||||||
return this.foo + this.self;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ObjectCacheService', () => {
|
describe('ObjectCacheService', () => {
|
||||||
let service: ObjectCacheService;
|
let service: ObjectCacheService;
|
||||||
@@ -26,7 +16,7 @@ describe('ObjectCacheService', () => {
|
|||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const objectToCache = {
|
const objectToCache = {
|
||||||
self: selfLink,
|
self: selfLink,
|
||||||
foo: 'bar'
|
type: ResourceType.Item
|
||||||
};
|
};
|
||||||
const cacheEntry = {
|
const cacheEntry = {
|
||||||
data: objectToCache,
|
data: objectToCache,
|
||||||
@@ -63,20 +53,20 @@ describe('ObjectCacheService', () => {
|
|||||||
it('should return an observable of the cached object with the specified self link and type', () => {
|
it('should return an observable of the cached object with the specified self link and type', () => {
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
|
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
|
||||||
|
|
||||||
let testObj: any;
|
|
||||||
// due to the implementation of spyOn above, this subscribe will be synchronous
|
// due to the implementation of spyOn above, this subscribe will be synchronous
|
||||||
service.getBySelfLink(selfLink, TestClass).take(1).subscribe((o) => testObj = o);
|
service.getBySelfLink(selfLink).take(1).subscribe((o) => {
|
||||||
expect(testObj.self).toBe(selfLink);
|
expect(o.self).toBe(selfLink);
|
||||||
expect(testObj.foo).toBe('bar');
|
// this only works if testObj is an instance of TestClass
|
||||||
// this only works if testObj is an instance of TestClass
|
expect(o instanceof NormalizedItem).toBeTruthy();
|
||||||
expect(testObj.test()).toBe('bar' + selfLink);
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return a cached object that has exceeded its time to live', () => {
|
it('should not return a cached object that has exceeded its time to live', () => {
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
|
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
|
||||||
|
|
||||||
let getObsHasFired = false;
|
let getObsHasFired = false;
|
||||||
const subscription = service.getBySelfLink(selfLink, TestClass).subscribe((o) => getObsHasFired = true);
|
const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true);
|
||||||
expect(getObsHasFired).toBe(false);
|
expect(getObsHasFired).toBe(false);
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
});
|
});
|
||||||
@@ -84,16 +74,14 @@ describe('ObjectCacheService', () => {
|
|||||||
|
|
||||||
describe('getList', () => {
|
describe('getList', () => {
|
||||||
it('should return an observable of the array of cached objects with the specified self link and type', () => {
|
it('should return an observable of the array of cached objects with the specified self link and type', () => {
|
||||||
spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(new TestClass(selfLink, 'bar')));
|
const item = new NormalizedItem();
|
||||||
|
item.self = selfLink;
|
||||||
|
spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(item));
|
||||||
|
|
||||||
let testObjs: any[];
|
service.getList([selfLink, selfLink]).take(1).subscribe((arr) => {
|
||||||
service.getList([selfLink, selfLink], TestClass).take(1).subscribe((arr) => testObjs = arr);
|
expect(arr[0].self).toBe(selfLink);
|
||||||
expect(testObjs[0].self).toBe(selfLink);
|
expect(arr[0] instanceof NormalizedItem).toBeTruthy();
|
||||||
expect(testObjs[0].foo).toBe('bar');
|
});
|
||||||
expect(testObjs[0].test()).toBe('bar' + selfLink);
|
|
||||||
expect(testObjs[1].self).toBe(selfLink);
|
|
||||||
expect(testObjs[1].foo).toBe('bar');
|
|
||||||
expect(testObjs[1].test()).toBe('bar' + selfLink);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
23
src/app/core/cache/object-cache.service.ts
vendored
23
src/app/core/cache/object-cache.service.ts
vendored
@@ -10,6 +10,9 @@ import { hasNoValue } from '../../shared/empty.util';
|
|||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { coreSelector, CoreState } from '../core.reducers';
|
import { coreSelector, CoreState } from '../core.reducers';
|
||||||
import { pathSelector } from '../shared/selectors';
|
import { pathSelector } from '../shared/selectors';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { NormalizedObjectFactory } from './models/normalized-object-factory';
|
||||||
|
import { NormalizedObject } from './models/normalized-object.model';
|
||||||
|
|
||||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
||||||
@@ -24,9 +27,8 @@ function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectCacheService {
|
export class ObjectCacheService {
|
||||||
constructor(
|
constructor(private store: Store<CoreState>) {
|
||||||
private store: Store<CoreState>
|
}
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an object to the cache
|
* Add an object to the cache
|
||||||
@@ -70,14 +72,17 @@ export class ObjectCacheService {
|
|||||||
* @return Observable<T>
|
* @return Observable<T>
|
||||||
* An observable of the requested object
|
* An observable of the requested object
|
||||||
*/
|
*/
|
||||||
getByUUID<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
|
getByUUID<T extends NormalizedObject>(uuid: string): Observable<T> {
|
||||||
return this.store.select(selfLinkFromUuidSelector(uuid))
|
return this.store.select(selfLinkFromUuidSelector(uuid))
|
||||||
.flatMap((selfLink: string) => this.getBySelfLink(selfLink, type))
|
.flatMap((selfLink: string) => this.getBySelfLink(selfLink))
|
||||||
}
|
}
|
||||||
|
|
||||||
getBySelfLink<T extends CacheableObject>(selfLink: string, type: GenericConstructor<T>): Observable<T> {
|
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
|
||||||
return this.getEntry(selfLink)
|
return this.getEntry(selfLink)
|
||||||
.map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T);
|
.map((entry: ObjectCacheEntry) => {
|
||||||
|
const type: GenericConstructor<NormalizedObject>= NormalizedObjectFactory.getConstructor(entry.data.type);
|
||||||
|
return Object.assign(new type(), entry.data) as T
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
||||||
@@ -116,9 +121,9 @@ export class ObjectCacheService {
|
|||||||
* The type of the objects to get
|
* The type of the objects to get
|
||||||
* @return Observable<Array<T>>
|
* @return Observable<Array<T>>
|
||||||
*/
|
*/
|
||||||
getList<T extends CacheableObject>(selfLinks: string[], type: GenericConstructor<T>): Observable<T[]> {
|
getList<T extends NormalizedObject>(selfLinks: string[]): Observable<T[]> {
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink, type))
|
selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
50
src/app/core/cache/response-cache.models.ts
vendored
50
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,7 +1,10 @@
|
|||||||
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { ConfigObject } from '../shared/config/config.model';
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
|
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||||
|
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -21,11 +24,52 @@ export class DSOSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointMap {
|
export class SearchSuccessResponse extends RestResponse {
|
||||||
[linkName: string]: string
|
constructor(
|
||||||
|
public results: SearchQueryResponse,
|
||||||
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RootSuccessResponse extends RestResponse {
|
export class FacetConfigSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public results: SearchFilterConfig[],
|
||||||
|
public statusCode: string
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FacetValueMap {
|
||||||
|
[name: string]: FacetValueSuccessResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FacetValueSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public results: FacetValue[],
|
||||||
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FacetValueMapSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public results: FacetValueMap,
|
||||||
|
public statusCode: string,
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EndpointMap {
|
||||||
|
[linkPath: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EndpointMapSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public endpointMap: EndpointMap,
|
public endpointMap: EndpointMap,
|
||||||
public statusCode: string,
|
public statusCode: string,
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/Rx';
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ConfigRequest, FindAllOptions } from '../data/request.models';
|
import { ConfigRequest, FindAllOptions } from '../data/request.models';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
const BROWSE = 'search/findByCollection';
|
const BROWSE = 'search/findByCollection';
|
||||||
|
|
||||||
class TestService extends ConfigService {
|
class TestService extends ConfigService {
|
||||||
protected linkName = LINK_NAME;
|
protected linkPath = LINK_NAME;
|
||||||
protected browseEndpoint = BROWSE;
|
protected browseEndpoint = BROWSE;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected EnvConfig: GlobalConfig
|
protected halService: HALEndpointService) {
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,8 +28,8 @@ describe('ConfigService', () => {
|
|||||||
let service: TestService;
|
let service: TestService;
|
||||||
let responseCache: ResponseCacheService;
|
let responseCache: ResponseCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
let halService: any;
|
||||||
|
|
||||||
const envConfig = {} as GlobalConfig;
|
|
||||||
const findOptions: FindAllOptions = new FindAllOptions();
|
const findOptions: FindAllOptions = new FindAllOptions();
|
||||||
|
|
||||||
const scopeName = 'traditional';
|
const scopeName = 'traditional';
|
||||||
@@ -51,7 +51,7 @@ describe('ConfigService', () => {
|
|||||||
return new TestService(
|
return new TestService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
envConfig
|
halService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +60,7 @@ describe('ConfigService', () => {
|
|||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
spyOn(service, 'getEndpoint').and
|
halService = new HALEndpointServiceStub(configEndpoint);
|
||||||
.returnValue(hot('--a-', { a: serviceEndpoint }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConfigByHref', () => {
|
describe('getConfigByHref', () => {
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
|
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
@@ -12,13 +9,13 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ConfigData } from './config-data';
|
import { ConfigData } from './config-data';
|
||||||
|
|
||||||
export abstract class ConfigService extends HALEndpointService {
|
export abstract class ConfigService {
|
||||||
protected request: ConfigRequest;
|
protected request: ConfigRequest;
|
||||||
protected abstract responseCache: ResponseCacheService;
|
protected abstract responseCache: ResponseCacheService;
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract linkName: string;
|
protected abstract linkPath: string;
|
||||||
protected abstract EnvConfig: GlobalConfig;
|
|
||||||
protected abstract browseEndpoint: string;
|
protected abstract browseEndpoint: string;
|
||||||
|
protected abstract halService: HALEndpointService;
|
||||||
|
|
||||||
protected getConfig(request: RestRequest): Observable<ConfigData> {
|
protected getConfig(request: RestRequest): Observable<ConfigData> {
|
||||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
@@ -58,11 +55,7 @@ export abstract class ConfigService extends HALEndpointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasValue(options.sort)) {
|
if (hasValue(options.sort)) {
|
||||||
let direction = 'asc';
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
if (options.sort.direction === 1) {
|
|
||||||
direction = 'desc';
|
|
||||||
}
|
|
||||||
args.push(`sort=${options.sort.field},${direction}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
@@ -72,7 +65,7 @@ export abstract class ConfigService extends HALEndpointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getConfigAll(): Observable<ConfigData> {
|
public getConfigAll(): Observable<ConfigData> {
|
||||||
return this.getEndpoint()
|
return this.halService.getEndpoint(this.linkPath)
|
||||||
.filter((href: string) => isNotEmpty(href))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
|
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
|
||||||
@@ -89,7 +82,7 @@ export abstract class ConfigService extends HALEndpointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getConfigByName(name: string): Observable<ConfigData> {
|
public getConfigByName(name: string): Observable<ConfigData> {
|
||||||
return this.getEndpoint()
|
return this.halService.getEndpoint(this.linkPath)
|
||||||
.map((endpoint: string) => this.getConfigByNameHref(endpoint, name))
|
.map((endpoint: string) => this.getConfigByNameHref(endpoint, name))
|
||||||
.filter((href: string) => isNotEmpty(href))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
@@ -100,7 +93,7 @@ export abstract class ConfigService extends HALEndpointService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
|
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
|
||||||
return this.getEndpoint()
|
return this.halService.getEndpoint(this.linkPath)
|
||||||
.map((endpoint: string) => this.getConfigSearchHref(endpoint, options))
|
.map((endpoint: string) => this.getConfigSearchHref(endpoint, options))
|
||||||
.filter((href: string) => isNotEmpty(href))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionDefinitionsConfigService extends ConfigService {
|
export class SubmissionDefinitionsConfigService extends ConfigService {
|
||||||
protected linkName = 'submissiondefinitions';
|
protected linkPath = 'submissiondefinitions';
|
||||||
protected browseEndpoint = 'search/findByCollection';
|
protected browseEndpoint = 'search/findByCollection';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionFormsConfigService extends ConfigService {
|
export class SubmissionFormsConfigService extends ConfigService {
|
||||||
protected linkName = 'submissionforms';
|
protected linkPath = 'submissionforms';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubmissionSectionsConfigService extends ConfigService {
|
export class SubmissionSectionsConfigService extends ConfigService {
|
||||||
protected linkName = 'submissionsections';
|
protected linkPath = 'submissionsections';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,5 +8,5 @@ export const coreEffects = [
|
|||||||
ResponseCacheEffects,
|
ResponseCacheEffects,
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
ObjectCacheEffects,
|
ObjectCacheEffects,
|
||||||
UUIDIndexEffects,
|
UUIDIndexEffects
|
||||||
];
|
];
|
||||||
|
@@ -17,7 +17,9 @@ import { isNotEmpty } from '../shared/empty.util';
|
|||||||
import { ApiService } from '../shared/api.service';
|
import { ApiService } from '../shared/api.service';
|
||||||
import { CollectionDataService } from './data/collection-data.service';
|
import { CollectionDataService } from './data/collection-data.service';
|
||||||
import { CommunityDataService } from './data/community-data.service';
|
import { CommunityDataService } from './data/community-data.service';
|
||||||
|
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
|
||||||
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
|
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
|
||||||
|
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
||||||
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { ItemDataService } from './data/item-data.service';
|
import { ItemDataService } from './data/item-data.service';
|
||||||
@@ -27,7 +29,7 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp
|
|||||||
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
|
||||||
import { RequestService } from './data/request.service';
|
import { RequestService } from './data/request.service';
|
||||||
import { ResponseCacheService } from './cache/response-cache.service';
|
import { ResponseCacheService } from './cache/response-cache.service';
|
||||||
import { RootResponseParsingService } from './data/root-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
|
||||||
import { ServerResponseService } from '../shared/server-response.service';
|
import { ServerResponseService } from '../shared/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
||||||
import { BrowseService } from './browse/browse.service';
|
import { BrowseService } from './browse/browse.service';
|
||||||
@@ -38,6 +40,10 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti
|
|||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||||
import { UUIDService } from './shared/uuid.service';
|
import { UUIDService } from './shared/uuid.service';
|
||||||
|
import { HALEndpointService } from './shared/hal-endpoint.service';
|
||||||
|
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||||
|
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||||
|
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
@@ -60,6 +66,7 @@ const PROVIDERS = [
|
|||||||
CollectionDataService,
|
CollectionDataService,
|
||||||
DSOResponseParsingService,
|
DSOResponseParsingService,
|
||||||
DSpaceRESTv2Service,
|
DSpaceRESTv2Service,
|
||||||
|
HALEndpointService,
|
||||||
HostWindowService,
|
HostWindowService,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
MetadataService,
|
MetadataService,
|
||||||
@@ -68,7 +75,12 @@ const PROVIDERS = [
|
|||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
RequestService,
|
RequestService,
|
||||||
ResponseCacheService,
|
ResponseCacheService,
|
||||||
RootResponseParsingService,
|
EndpointMapResponseParsingService,
|
||||||
|
FacetValueResponseParsingService,
|
||||||
|
FacetValueMapResponseParsingService,
|
||||||
|
FacetConfigResponseParsingService,
|
||||||
|
DebugResponseParsingService,
|
||||||
|
SearchResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
|
@@ -117,9 +117,14 @@ export abstract class BaseResponseParsingService {
|
|||||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processPageInfo(pageObj: any): PageInfo {
|
processPageInfo(payload: any): PageInfo {
|
||||||
if (isNotEmpty(pageObj)) {
|
if (isNotEmpty(payload.page)) {
|
||||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
const pageObj = Object.assign({}, payload.page, {_links: payload._links});
|
||||||
|
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||||
|
if (pageInfoObject.currentPage >= 0) {
|
||||||
|
Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 });
|
||||||
|
}
|
||||||
|
return pageInfoObject
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@@ -10,20 +10,21 @@ import { Collection } from '../shared/collection.model';
|
|||||||
import { ComColDataService } from './comcol-data.service';
|
import { ComColDataService } from './comcol-data.service';
|
||||||
import { CommunityDataService } from './community-data.service';
|
import { CommunityDataService } from './community-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
||||||
protected linkName = 'collections';
|
protected linkPath = 'collections';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
protected objectCache: ObjectCacheService
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCollection);
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,6 @@ import { GlobalConfig } from '../../../config';
|
|||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -13,16 +12,16 @@ import { ComColDataService } from './comcol-data.service';
|
|||||||
import { CommunityDataService } from './community-data.service';
|
import { CommunityDataService } from './community-data.service';
|
||||||
import { FindByIDRequest } from './request.models';
|
import { FindByIDRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
class NormalizedTestObject implements CacheableObject {
|
class NormalizedTestObject extends NormalizedObject {
|
||||||
self: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestService extends ComColDataService<NormalizedTestObject, any> {
|
class TestService extends ComColDataService<NormalizedTestObject, any> {
|
||||||
protected linkName = LINK_NAME;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
@@ -31,9 +30,11 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
|
|||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected EnvConfig: GlobalConfig,
|
protected EnvConfig: GlobalConfig,
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
protected objectCache: ObjectCacheService
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected linkPath: string
|
||||||
) {
|
) {
|
||||||
super(NormalizedTestObject);
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
@@ -45,6 +46,7 @@ describe('ComColDataService', () => {
|
|||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let cds: CommunityDataService;
|
let cds: CommunityDataService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
|
const halService: any = {};
|
||||||
|
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
@@ -91,7 +93,9 @@ describe('ComColDataService', () => {
|
|||||||
store,
|
store,
|
||||||
EnvConfig,
|
EnvConfig,
|
||||||
cds,
|
cds,
|
||||||
objectCache
|
objectCache,
|
||||||
|
halService,
|
||||||
|
LINK_NAME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +131,7 @@ describe('ComColDataService', () => {
|
|||||||
it('should fetch the scope Community from the cache', () => {
|
it('should fetch the scope Community from the cache', () => {
|
||||||
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID, NormalizedCommunity);
|
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the endpoint to fetch resources within the given scope', () => {
|
it('should return the endpoint to fetch resources within the given scope', () => {
|
||||||
@@ -155,24 +159,5 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if the scope is not specified', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cds = initMockCommunityDataService();
|
|
||||||
requestService = getMockRequestService();
|
|
||||||
objectCache = initMockObjectCacheService();
|
|
||||||
responseCache = initMockResponseCacheService(true);
|
|
||||||
service = initTestService();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return this.getEndpoint()', () => {
|
|
||||||
spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint }));
|
|
||||||
|
|
||||||
const result = service.getScopedEndpoint(undefined);
|
|
||||||
const expected = cold('--f-', { f: serviceEndpoint });
|
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -9,15 +9,18 @@ import { CommunityDataService } from './community-data.service';
|
|||||||
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { FindByIDRequest } from './request.models';
|
import { FindByIDRequest } from './request.models';
|
||||||
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
export abstract class ComColDataService<TNormalized extends CacheableObject, TDomain> extends DataService<TNormalized, TDomain> {
|
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
|
||||||
protected abstract cds: CommunityDataService;
|
protected abstract cds: CommunityDataService;
|
||||||
protected abstract objectCache: ObjectCacheService;
|
protected abstract objectCache: ObjectCacheService;
|
||||||
|
protected abstract halService: HALEndpointService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the scoped endpoint URL by fetching the object with
|
* Get the scoped endpoint URL by fetching the object with
|
||||||
* the given scopeID and returning its HAL link with this
|
* the given scopeID and returning its HAL link with this
|
||||||
* data-service's linkName
|
* data-service's linkPath
|
||||||
*
|
*
|
||||||
* @param {string} scopeID
|
* @param {string} scopeID
|
||||||
* the id of the scope object
|
* the id of the scope object
|
||||||
@@ -26,7 +29,7 @@ export abstract class ComColDataService<TNormalized extends CacheableObject, TDo
|
|||||||
*/
|
*/
|
||||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
if (isEmpty(scopeID)) {
|
if (isEmpty(scopeID)) {
|
||||||
return this.getEndpoint();
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
} else {
|
} else {
|
||||||
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
||||||
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
|
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
|
||||||
@@ -47,8 +50,8 @@ export abstract class ComColDataService<TNormalized extends CacheableObject, TDo
|
|||||||
errorResponse.flatMap((response: ErrorResponse) =>
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
|
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
|
||||||
successResponse
|
successResponse
|
||||||
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity))
|
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID))
|
||||||
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
|
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
|
||||||
.filter((href) => isNotEmpty(href))
|
.filter((href) => isNotEmpty(href))
|
||||||
).distinctUntilChanged();
|
).distinctUntilChanged();
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,11 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
import { ComColDataService } from './comcol-data.service';
|
import { ComColDataService } from './comcol-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
||||||
protected linkName = 'communities';
|
protected linkPath = 'communities';
|
||||||
protected cds = this;
|
protected cds = this;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -21,9 +22,13 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
|
|||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
protected objectCache: ObjectCacheService,
|
||||||
protected objectCache: ObjectCacheService
|
protected halService: HALEndpointService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCommunity);
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint() {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
|
|||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
|
||||||
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
|
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
|
||||||
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
|
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@@ -1,32 +1,24 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
|
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
|
||||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService {
|
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
|
||||||
protected abstract responseCache: ResponseCacheService;
|
protected abstract responseCache: ResponseCacheService;
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
protected abstract linkName: string;
|
protected abstract linkPath: string;
|
||||||
protected abstract EnvConfig: GlobalConfig;
|
protected abstract halService: HALEndpointService;
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected normalizedResourceType: GenericConstructor<TNormalized>,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract getScopedEndpoint(scope: string): Observable<string>
|
public abstract getScopedEndpoint(scope: string): Observable<string>
|
||||||
|
|
||||||
@@ -50,11 +42,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasValue(options.sort)) {
|
if (hasValue(options.sort)) {
|
||||||
let direction = 'asc';
|
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||||
if (options.sort.direction === 1) {
|
|
||||||
direction = 'desc';
|
|
||||||
}
|
|
||||||
args.push(`sort=${options.sort.field},${direction}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
@@ -65,7 +53,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
}
|
}
|
||||||
|
|
||||||
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
|
||||||
const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href))
|
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
|
||||||
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
@@ -76,7 +64,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType) as Observable<RemoteData<PaginatedList<TDomain>>>;
|
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs) as Observable<RemoteData<PaginatedList<TDomain>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFindByIDHref(endpoint, resourceID): string {
|
getFindByIDHref(endpoint, resourceID): string {
|
||||||
@@ -84,7 +72,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Observable<RemoteData<TDomain>> {
|
findById(id: string): Observable<RemoteData<TDomain>> {
|
||||||
const hrefObs = this.getEndpoint()
|
const hrefObs = this.halService.getEndpoint(this.linkPath)
|
||||||
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
|
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
@@ -95,12 +83,12 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByHref(href: string): Observable<RemoteData<TDomain>> {
|
findByHref(href: string): Observable<RemoteData<TDomain>> {
|
||||||
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href));
|
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href));
|
||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO implement, after the structure of the REST server's POST response is finalized
|
// TODO implement, after the structure of the REST server's POST response is finalized
|
||||||
|
13
src/app/core/data/debug-response-parsing.service.ts
Normal file
13
src/app/core/data/debug-response-parsing.service.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DebugResponseParsingService implements ResponseParsingService {
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
console.log('request', request, 'data', data);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -28,7 +28,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
|
|||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href);
|
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href);
|
||||||
const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
|
const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
|
||||||
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
|
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { ErrorResponse, RestResponse, RootSuccessResponse } from '../cache/response-cache.models';
|
import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RootResponseParsingService implements ResponseParsingService {
|
export class EndpointMapResponseParsingService implements ResponseParsingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
||||||
) {
|
) {
|
||||||
@@ -21,7 +20,7 @@ export class RootResponseParsingService implements ResponseParsingService {
|
|||||||
for (const link of Object.keys(links)) {
|
for (const link of Object.keys(links)) {
|
||||||
links[link] = links[link].href;
|
links[link] = links[link].href;
|
||||||
}
|
}
|
||||||
return new RootSuccessResponse(links, data.statusCode);
|
return new EndpointMapSuccessResponse(links, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
32
src/app/core/data/facet-config-response-parsing.service.ts
Normal file
32
src/app/core/data/facet-config-response-parsing.service.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
FacetConfigSuccessResponse,
|
||||||
|
RestResponse
|
||||||
|
} from '../cache/response-cache.models';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
objectFactory = {};
|
||||||
|
toCache = false;
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
|
||||||
|
const config = data.payload._embedded.facets;
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig);
|
||||||
|
const facetConfig = serializer.deserializeArray(config);
|
||||||
|
return new FacetConfigSuccessResponse(facetConfig, data.statusCode);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
FacetValueMap,
|
||||||
|
FacetValueMapSuccessResponse,
|
||||||
|
FacetValueSuccessResponse,
|
||||||
|
RestResponse
|
||||||
|
} from '../cache/response-cache.models';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
objectFactory = {};
|
||||||
|
toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
|
||||||
|
const payload = data.payload;
|
||||||
|
const facetMap: FacetValueMap = new FacetValueMap();
|
||||||
|
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(FacetValue);
|
||||||
|
payload._embedded.facets.map((facet) => {
|
||||||
|
const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
|
||||||
|
const facetValues = serializer.deserializeArray(values);
|
||||||
|
const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
|
||||||
|
facetMap[facet.name] = valuesResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new FacetValueMapSuccessResponse(facetMap, data.statusCode);
|
||||||
|
}
|
||||||
|
}
|
38
src/app/core/data/facet-value-response-parsing.service.ts
Normal file
38
src/app/core/data/facet-value-response-parsing.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
FacetValueMap,
|
||||||
|
FacetValueMapSuccessResponse,
|
||||||
|
FacetValueSuccessResponse,
|
||||||
|
RestResponse
|
||||||
|
} from '../cache/response-cache.models';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
objectFactory = {};
|
||||||
|
toCache = false;
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(FacetValue);
|
||||||
|
// const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
|
||||||
|
|
||||||
|
const facetValues = serializer.deserializeArray(payload._embedded.values);
|
||||||
|
return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,24 +1,23 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/Rx';
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ItemDataService;
|
let service: ItemDataService;
|
||||||
let bs: BrowseService;
|
let bs: BrowseService;
|
||||||
|
|
||||||
const requestService = {} as RequestService;
|
const requestService = {} as RequestService;
|
||||||
const responseCache = {} as ResponseCacheService;
|
const responseCache = {} as ResponseCacheService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const EnvConfig = {} as GlobalConfig;
|
const halEndpointService = {} as HALEndpointService;
|
||||||
|
|
||||||
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
const browsesEndpoint = 'https://rest.api/discover/browses';
|
const browsesEndpoint = 'https://rest.api/discover/browses';
|
||||||
@@ -42,8 +41,8 @@ describe('ItemDataService', () => {
|
|||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
store,
|
store,
|
||||||
EnvConfig,
|
bs,
|
||||||
bs
|
halEndpointService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,21 +73,5 @@ describe('ItemDataService', () => {
|
|||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if the scope is not specified', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
bs = initMockBrowseService(true);
|
|
||||||
service = initTestService();
|
|
||||||
spyOn(service, 'getEndpoint').and.returnValue(cold('--b-', { b: serviceEndpoint }))
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return this.getEndpoint()', () => {
|
|
||||||
const result = service.getScopedEndpoint(undefined);
|
|
||||||
const expected = cold('--c-', { c: serviceEndpoint });
|
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -14,27 +14,27 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
protected linkName = 'items';
|
protected linkPath = 'items';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
private bs: BrowseService,
|
||||||
private bs: BrowseService
|
protected halService: HALEndpointService) {
|
||||||
) {
|
super();
|
||||||
super(NormalizedItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScopedEndpoint(scopeID: string): Observable<string> {
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
if (isEmpty(scopeID)) {
|
if (isEmpty(scopeID)) {
|
||||||
return this.getEndpoint();
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
} else {
|
} else {
|
||||||
return this.bs.getBrowseURLFor('dc.date.issued', this.linkName)
|
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath)
|
||||||
.filter((href: string) => isNotEmpty(href))
|
.filter((href: string) => isNotEmpty(href))
|
||||||
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
|
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
export class PaginatedList<T> {
|
export class PaginatedList<T> {
|
||||||
|
|
||||||
constructor(
|
constructor(private pageInfo: PageInfo,
|
||||||
private pageInfo: PageInfo,
|
public page: T[]) {
|
||||||
public page: T[]
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get elementsPerPage(): number {
|
get elementsPerPage(): number {
|
||||||
return this.pageInfo.elementsPerPage;
|
if (hasValue(this.pageInfo)) {
|
||||||
|
return this.pageInfo.elementsPerPage;
|
||||||
|
}
|
||||||
|
return this.page.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
set elementsPerPage(value: number) {
|
set elementsPerPage(value: number) {
|
||||||
@@ -17,7 +19,10 @@ export class PaginatedList<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get totalElements(): number {
|
get totalElements(): number {
|
||||||
return this.pageInfo.totalElements;
|
if (hasValue(this.pageInfo)) {
|
||||||
|
return this.pageInfo.totalElements;
|
||||||
|
}
|
||||||
|
return this.page.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
set totalElements(value: number) {
|
set totalElements(value: number) {
|
||||||
@@ -25,7 +30,10 @@ export class PaginatedList<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return this.pageInfo.totalPages;
|
if (hasValue(this.pageInfo)) {
|
||||||
|
return this.pageInfo.totalPages;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
set totalPages(value: number) {
|
set totalPages(value: number) {
|
||||||
@@ -33,10 +41,44 @@ export class PaginatedList<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get currentPage(): number {
|
get currentPage(): number {
|
||||||
return this.pageInfo.currentPage;
|
if (hasValue(this.pageInfo)) {
|
||||||
|
return this.pageInfo.currentPage;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
set currentPage(value: number) {
|
set currentPage(value: number) {
|
||||||
this.pageInfo.currentPage = value;
|
this.pageInfo.currentPage = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get first(): string {
|
||||||
|
return this.pageInfo.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
set first(first: string) {
|
||||||
|
this.pageInfo.first = first;
|
||||||
|
}
|
||||||
|
|
||||||
|
get prev(): string {
|
||||||
|
return this.pageInfo.prev;
|
||||||
|
}
|
||||||
|
set prev(prev: string) {
|
||||||
|
this.pageInfo.prev = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
get next(): string {
|
||||||
|
return this.pageInfo.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
set next(next: string) {
|
||||||
|
this.pageInfo.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
get last(): string {
|
||||||
|
return this.pageInfo.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
set last(last: string) {
|
||||||
|
this.pageInfo.last = last;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { GlobalConfig } from '../../../config/global-config.interface';
|
|||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RootResponseParsingService } from './root-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||||
|
|
||||||
@@ -140,14 +140,17 @@ export class FindAllRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RootEndpointRequest extends GetRequest {
|
export class EndpointMapRequest extends GetRequest {
|
||||||
constructor(uuid: string, EnvConfig: GlobalConfig) {
|
constructor(
|
||||||
const href = new RESTURLCombiner(EnvConfig, '/').toString();
|
public uuid: string,
|
||||||
super(uuid, href);
|
public href: string,
|
||||||
|
public body?: any
|
||||||
|
) {
|
||||||
|
super(uuid, href, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
return RootResponseParsingService;
|
return EndpointMapResponseParsingService;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
src/app/core/data/search-response-parsing.service.ts
Normal file
61
src/app/core/data/search-response-parsing.service.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RestResponse, SearchSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||||
|
import { Metadatum } from '../shared/metadatum.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchResponseParsingService implements ResponseParsingService {
|
||||||
|
constructor(private dsoParser: DSOResponseParsingService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
const hitHighlights = payload._embedded.objects
|
||||||
|
.map((object) => object.hitHighlights)
|
||||||
|
.map((hhObject) => {
|
||||||
|
if (hhObject) {
|
||||||
|
return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), {
|
||||||
|
key: key,
|
||||||
|
value: hhObject[key].join('...')
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dsoSelfLinks = payload._embedded.objects
|
||||||
|
.filter((object) => hasValue(object._embedded))
|
||||||
|
.map((object) => object._embedded.dspaceObject)
|
||||||
|
// we don't need embedded collections, bitstreamformats, etc for search results.
|
||||||
|
// And parsing them all takes up a lot of time. Throw them away to improve performance
|
||||||
|
// until objs until partial results are supported by the rest api
|
||||||
|
.map((dso) => Object.assign({}, dso, { _embedded: undefined }))
|
||||||
|
.map((dso) => this.dsoParser.parse(request, {
|
||||||
|
payload: dso,
|
||||||
|
statusCode: data.statusCode
|
||||||
|
}))
|
||||||
|
.map((obj) => obj.resourceSelfLinks)
|
||||||
|
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
|
||||||
|
|
||||||
|
const objects = payload._embedded.objects
|
||||||
|
.filter((object) => hasValue(object._embedded))
|
||||||
|
.map((object, index) => Object.assign({}, object, {
|
||||||
|
dspaceObject: dsoSelfLinks[index],
|
||||||
|
hitHighlights: hitHighlights[index],
|
||||||
|
// we don't need embedded collections, bitstreamformats, etc for search results.
|
||||||
|
// And parsing them all takes up a lot of time. Throw them away to improve performance
|
||||||
|
// until objs until partial results are supported by the rest api
|
||||||
|
_embedded: undefined
|
||||||
|
}));
|
||||||
|
payload.objects = objects;
|
||||||
|
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
|
||||||
|
return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
|
||||||
|
}
|
||||||
|
}
|
@@ -33,7 +33,7 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { MockItem } from '../../shared/mocks/mock-item';
|
import { MockItem } from '../../shared/mocks/mock-item';
|
||||||
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -114,6 +114,7 @@ describe('MetadataService', () => {
|
|||||||
{ provide: RequestService, useValue: requestService },
|
{ provide: RequestService, useValue: requestService },
|
||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
|
{ provide: HALEndpointService, useValue: {}},
|
||||||
Meta,
|
Meta,
|
||||||
Title,
|
Title,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
@@ -1,17 +1,24 @@
|
|||||||
import { DSpaceObject } from './dspace-object.model';
|
|
||||||
|
|
||||||
export class BitstreamFormat extends DSpaceObject {
|
import { autoserialize } from 'cerialize';
|
||||||
|
|
||||||
|
export class BitstreamFormat {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
mimetype: string;
|
mimetype: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
supportLevel: number;
|
supportLevel: number;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
internal: boolean;
|
internal: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
extensions: string;
|
extensions: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
/**
|
/**
|
||||||
* An abstract model class for a DSpaceObject.
|
* An abstract model class for a DSpaceObject.
|
||||||
*/
|
*/
|
||||||
export abstract class DSpaceObject implements CacheableObject, ListableObject {
|
export class DSpaceObject implements CacheableObject, ListableObject {
|
||||||
|
|
||||||
self: string;
|
self: string;
|
||||||
|
|
||||||
|
@@ -2,9 +2,9 @@ import { cold, hot } from 'jasmine-marbles';
|
|||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { RootEndpointRequest } from '../data/request.models';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from './hal-endpoint.service';
|
import { HALEndpointService } from './hal-endpoint.service';
|
||||||
|
import { EndpointMapRequest } from '../data/request.models';
|
||||||
|
|
||||||
describe('HALEndpointService', () => {
|
describe('HALEndpointService', () => {
|
||||||
let service: HALEndpointService;
|
let service: HALEndpointService;
|
||||||
@@ -15,24 +15,12 @@ describe('HALEndpointService', () => {
|
|||||||
const endpointMap = {
|
const endpointMap = {
|
||||||
test: 'https://rest.api/test',
|
test: 'https://rest.api/test',
|
||||||
};
|
};
|
||||||
|
const linkPath = 'test';
|
||||||
|
|
||||||
/* tslint:disable:no-shadowed-variable */
|
describe('getRootEndpointMap', () => {
|
||||||
class TestService extends HALEndpointService {
|
|
||||||
protected linkName = 'test';
|
|
||||||
|
|
||||||
constructor(protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
|
||||||
protected EnvConfig: GlobalConfig) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:enable:no-shadowed-variable */
|
|
||||||
|
|
||||||
describe('getEndpointMap', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = jasmine.createSpyObj('responseCache', {
|
responseCache = jasmine.createSpyObj('responseCache', {
|
||||||
get: hot('--a-', {
|
get: hot('a-', {
|
||||||
a: {
|
a: {
|
||||||
response: { endpointMap: endpointMap }
|
response: { endpointMap: endpointMap }
|
||||||
}
|
}
|
||||||
@@ -45,57 +33,62 @@ describe('HALEndpointService', () => {
|
|||||||
rest: { baseUrl: 'https://rest.api/' }
|
rest: { baseUrl: 'https://rest.api/' }
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
service = new TestService(
|
service = new HALEndpointService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
envConfig
|
envConfig
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure a new RootEndpointRequest', () => {
|
it('should configure a new EndpointMapRequest', () => {
|
||||||
(service as any).getEndpointMap();
|
(service as any).getRootEndpointMap();
|
||||||
const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig);
|
const expected = new EndpointMapRequest(requestService.generateRequestId(), envConfig.rest.baseUrl);
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an Observable of the endpoint map', () => {
|
it('should return an Observable of the endpoint map', () => {
|
||||||
const result = (service as any).getEndpointMap();
|
const result = (service as any).getRootEndpointMap();
|
||||||
const expected = cold('--b-', { b: endpointMap });
|
const expected = cold('b-', { b: endpointMap });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getEndpoint', () => {
|
describe('getEndpoint', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new TestService(
|
envConfig = {
|
||||||
|
rest: { baseUrl: 'https://rest.api/' }
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
service = new HALEndpointService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
envConfig
|
envConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
spyOn(service as any, 'getEndpointMap').and
|
|
||||||
.returnValue(hot('--a-', { a: endpointMap }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the endpoint URL for the service\'s linkName', () => {
|
it('should return the endpoint URL for the service\'s linkPath', () => {
|
||||||
const result = service.getEndpoint();
|
spyOn(service as any, 'getEndpointAt').and
|
||||||
const expected = cold('--b-', { b: endpointMap.test });
|
.returnValue(hot('a-', { a: 'https://rest.api/test' }));
|
||||||
|
const result = service.getEndpoint(linkPath);
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: endpointMap.test });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined for a linkName that isn\'t in the endpoint map', () => {
|
it('should return undefined for a linkPath that isn\'t in the endpoint map', () => {
|
||||||
(service as any).linkName = 'unknown';
|
spyOn(service as any, 'getEndpointAt').and
|
||||||
const result = service.getEndpoint();
|
.returnValue(hot('a-', { a: undefined }));
|
||||||
const expected = cold('--b-', { b: undefined });
|
const result = service.getEndpoint('unknown');
|
||||||
|
const expected = cold('b-', { b: undefined });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isEnabledOnRestApi', () => {
|
describe('isEnabledOnRestApi', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new TestService(
|
service = new HALEndpointService(
|
||||||
responseCache,
|
responseCache,
|
||||||
requestService,
|
requestService,
|
||||||
envConfig
|
envConfig
|
||||||
@@ -103,31 +96,29 @@ describe('HALEndpointService', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined as long as getEndpointMap hasn\'t fired', () => {
|
it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => {
|
||||||
spyOn(service as any, 'getEndpointMap').and
|
spyOn(service as any, 'getRootEndpointMap').and
|
||||||
.returnValue(hot('----'));
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
const result = service.isEnabledOnRestApi();
|
const result = service.isEnabledOnRestApi(linkPath);
|
||||||
const expected = cold('b---', { b: undefined });
|
const expected = cold('b---', { b: undefined });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if the service\'s linkName is in the endpoint map', () => {
|
it('should return true if the service\'s linkPath is in the endpoint map', () => {
|
||||||
spyOn(service as any, 'getEndpointMap').and
|
spyOn(service as any, 'getRootEndpointMap').and
|
||||||
.returnValue(hot('--a-', { a: endpointMap }));
|
.returnValue(hot('--a-', { a: endpointMap }));
|
||||||
|
const result = service.isEnabledOnRestApi(linkPath);
|
||||||
const result = service.isEnabledOnRestApi();
|
|
||||||
const expected = cold('b-c-', { b: undefined, c: true });
|
const expected = cold('b-c-', { b: undefined, c: true });
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the service\'s linkName isn\'t in the endpoint map', () => {
|
it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => {
|
||||||
spyOn(service as any, 'getEndpointMap').and
|
spyOn(service as any, 'getRootEndpointMap').and
|
||||||
.returnValue(hot('--a-', { a: endpointMap }));
|
.returnValue(hot('--a-', { a: endpointMap }));
|
||||||
|
|
||||||
(service as any).linkName = 'unknown';
|
const result = service.isEnabledOnRestApi('unknown');
|
||||||
const result = service.isEnabledOnRestApi();
|
const expected = cold('b-c-', { b: undefined, c: false });
|
||||||
const expected = cold('b-c-', { b: undefined, c: false });
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,39 +1,76 @@
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
|
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { RootEndpointRequest } from '../data/request.models';
|
import { EndpointMapRequest } from '../data/request.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
|
||||||
export abstract class HALEndpointService {
|
@Injectable()
|
||||||
protected abstract responseCache: ResponseCacheService;
|
export class HALEndpointService {
|
||||||
protected abstract requestService: RequestService;
|
|
||||||
protected abstract linkName: string;
|
|
||||||
protected abstract EnvConfig: GlobalConfig;
|
|
||||||
|
|
||||||
protected getEndpointMap(): Observable<EndpointMap> {
|
constructor(private responseCache: ResponseCacheService,
|
||||||
const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig);
|
private requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getRootHref(): string {
|
||||||
|
return new RESTURLCombiner(this.EnvConfig, '/').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getRootEndpointMap(): Observable<EndpointMap> {
|
||||||
|
return this.getEndpointMapAt(this.getRootHref());
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEndpointMapAt(href): Observable<EndpointMap> {
|
||||||
|
const request = new EndpointMapRequest(this.requestService.generateRequestId(), href);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
return this.responseCache.get(request.href)
|
return this.responseCache.get(request.href)
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
.filter((response: EndpointMapSuccessResponse) => isNotEmpty(response))
|
||||||
.map((response: RootSuccessResponse) => response.endpointMap)
|
.map((response: EndpointMapSuccessResponse) => response.endpointMap)
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEndpoint(): Observable<string> {
|
public getEndpoint(linkPath: string): Observable<string> {
|
||||||
return this.getEndpointMap()
|
return this.getEndpointAt(...linkPath.split('/'));
|
||||||
.map((map: EndpointMap) => map[this.linkName])
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEnabledOnRestApi(): Observable<boolean> {
|
private getEndpointAt(...path: string[]): Observable<string> {
|
||||||
return this.getEndpointMap()
|
if (isEmpty(path)) {
|
||||||
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
|
path = ['/'];
|
||||||
.startWith(undefined)
|
}
|
||||||
.distinctUntilChanged();
|
let currentPath;
|
||||||
|
const pipeArguments = path
|
||||||
|
.map((subPath: string, index: number) => [
|
||||||
|
flatMap((href: string) => this.getEndpointMapAt(href)),
|
||||||
|
map((endpointMap: EndpointMap) => {
|
||||||
|
if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) {
|
||||||
|
currentPath = endpointMap[subPath];
|
||||||
|
return endpointMap[subPath];
|
||||||
|
} else {
|
||||||
|
/*TODO remove if/else block once the rest response contains _links for facets*/
|
||||||
|
currentPath += '/' + subPath;
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
|
||||||
|
return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabledOnRestApi(linkPath: string): Observable<boolean> {
|
||||||
|
return this.getRootEndpointMap().pipe(
|
||||||
|
// TODO this only works when there's no / in linkPath
|
||||||
|
map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[linkPath])),
|
||||||
|
startWith(undefined),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -28,4 +28,15 @@ export class PageInfo {
|
|||||||
@autoserializeAs(Number, 'number')
|
@autoserializeAs(Number, 'number')
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
last: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
next: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
prev: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
first: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
/**
|
|
||||||
* TODO replace with actual string enum after upgrade to TypeScript 2.4:
|
|
||||||
* https://github.com/Microsoft/TypeScript/pull/15486
|
|
||||||
*/
|
|
||||||
export enum ResourceType {
|
export enum ResourceType {
|
||||||
|
DSpaceObject = 'dspaceobject',
|
||||||
Bundle = 'bundle',
|
Bundle = 'bundle',
|
||||||
Bitstream = 'bitstream',
|
Bitstream = 'bitstream',
|
||||||
BitstreamFormat = 'bitstreamformat',
|
BitstreamFormat = 'bitstreamformat',
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { HostWindowState } from './host-window.reducer';
|
import { HostWindowState } from './host-window.reducer';
|
||||||
|
|
||||||
import { HostWindowService } from './host-window.service';
|
import { GridBreakpoint, HostWindowService, WidthCategory } from './host-window.service';
|
||||||
|
|
||||||
describe('HostWindowService', () => {
|
describe('HostWindowService', () => {
|
||||||
let service: HostWindowService;
|
let service: HostWindowService;
|
||||||
@@ -189,4 +190,76 @@ describe('HostWindowService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('widthCategory', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new HostWindowService({} as Store<AppState>);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getWithObs to get the current width', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', { a: GridBreakpoint.SM_MIN - 1 }));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
expect((service as any).getWidthObs).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return XS if width < SM_MIN', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', { a: GridBreakpoint.SM_MIN - 1 }));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: WidthCategory.XS });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return SM if SM_MIN <= width < MD_MIN', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', {
|
||||||
|
a: GridBreakpoint.SM_MIN + Math.floor((GridBreakpoint.MD_MIN - GridBreakpoint.SM_MIN) / 2)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: WidthCategory.SM });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return MD if MD_MIN <= width < LG_MIN', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', {
|
||||||
|
a: GridBreakpoint.MD_MIN + Math.floor((GridBreakpoint.LG_MIN - GridBreakpoint.MD_MIN) / 2)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: WidthCategory.MD });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return LG if LG_MIN <= width < XL_MIN', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', {
|
||||||
|
a: GridBreakpoint.LG_MIN + Math.floor((GridBreakpoint.XL_MIN - GridBreakpoint.LG_MIN) / 2)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: WidthCategory.LG });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return XL if width >= XL_MIN', () => {
|
||||||
|
spyOn(service as any, 'getWidthObs').and
|
||||||
|
.returnValue(hot('a-', { a: GridBreakpoint.XL_MIN + 1 }));
|
||||||
|
|
||||||
|
const result = service.widthCategory;
|
||||||
|
|
||||||
|
const expected = cold('b-', { b: WidthCategory.XL });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { HostWindowState } from './host-window.reducer';
|
import { HostWindowState } from './host-window.reducer';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { createSelector, Store } from '@ngrx/store';
|
import { createSelector, Store } from '@ngrx/store';
|
||||||
@@ -8,11 +9,18 @@ import { AppState } from '../app.reducer';
|
|||||||
|
|
||||||
// TODO: ideally we should get these from sass somehow
|
// TODO: ideally we should get these from sass somehow
|
||||||
export enum GridBreakpoint {
|
export enum GridBreakpoint {
|
||||||
XS = 0,
|
SM_MIN = 576,
|
||||||
SM = 576,
|
MD_MIN = 768,
|
||||||
MD = 768,
|
LG_MIN = 992,
|
||||||
LG = 992,
|
XL_MIN = 1200
|
||||||
XL = 1200
|
}
|
||||||
|
|
||||||
|
export enum WidthCategory {
|
||||||
|
XS,
|
||||||
|
SM,
|
||||||
|
MD,
|
||||||
|
LG,
|
||||||
|
XL
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostWindowStateSelector = (state: AppState) => state.hostWindow;
|
const hostWindowStateSelector = (state: AppState) => state.hostWindow;
|
||||||
@@ -31,33 +39,57 @@ export class HostWindowService {
|
|||||||
.filter((width) => hasValue(width));
|
.filter((width) => hasValue(width));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get widthCategory(): Observable<WidthCategory> {
|
||||||
|
return this.getWidthObs().pipe(
|
||||||
|
map((width: number) => {
|
||||||
|
if (width < GridBreakpoint.SM_MIN) {
|
||||||
|
return WidthCategory.XS
|
||||||
|
} else if (width >= GridBreakpoint.SM_MIN && width < GridBreakpoint.MD_MIN) {
|
||||||
|
return WidthCategory.SM
|
||||||
|
} else if (width >= GridBreakpoint.MD_MIN && width < GridBreakpoint.LG_MIN) {
|
||||||
|
return WidthCategory.MD
|
||||||
|
} else if (width >= GridBreakpoint.LG_MIN && width < GridBreakpoint.XL_MIN) {
|
||||||
|
return WidthCategory.LG
|
||||||
|
} else {
|
||||||
|
return WidthCategory.XL
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
isXs(): Observable<boolean> {
|
isXs(): Observable<boolean> {
|
||||||
return this.getWidthObs()
|
return this.widthCategory.pipe(
|
||||||
.map((width) => width < GridBreakpoint.SM)
|
map((widthCat: WidthCategory) => widthCat === WidthCategory.XS),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSm(): Observable<boolean> {
|
isSm(): Observable<boolean> {
|
||||||
return this.getWidthObs()
|
return this.widthCategory.pipe(
|
||||||
.map((width) => width >= GridBreakpoint.SM && width < GridBreakpoint.MD)
|
map((widthCat: WidthCategory) => widthCat === WidthCategory.SM),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isMd(): Observable<boolean> {
|
isMd(): Observable<boolean> {
|
||||||
return this.getWidthObs()
|
return this.widthCategory.pipe(
|
||||||
.map((width) => width >= GridBreakpoint.MD && width < GridBreakpoint.LG)
|
map((widthCat: WidthCategory) => widthCat === WidthCategory.MD),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLg(): Observable<boolean> {
|
isLg(): Observable<boolean> {
|
||||||
return this.getWidthObs()
|
return this.widthCategory.pipe(
|
||||||
.map((width) => width >= GridBreakpoint.LG && width < GridBreakpoint.XL)
|
map((widthCat: WidthCategory) => widthCat === WidthCategory.LG),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isXl(): Observable<boolean> {
|
isXl(): Observable<boolean> {
|
||||||
return this.getWidthObs()
|
return this.widthCategory.pipe(
|
||||||
.map((width) => width >= GridBreakpoint.XL)
|
map((widthCat: WidthCategory) => widthCat === WidthCategory.XL),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
src/app/shared/mocks/mock-angulartics.service.ts
Normal file
4
src/app/shared/mocks/mock-angulartics.service.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* tslint:disable:no-empty */
|
||||||
|
export class AngularticsMock {
|
||||||
|
public eventTrack(action, properties) { }
|
||||||
|
}
|
@@ -1,8 +1,10 @@
|
|||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
|
||||||
export function getMockRequestService(): RequestService {
|
export function getMockRequestService(): RequestService {
|
||||||
return jasmine.createSpyObj('requestService', {
|
return jasmine.createSpyObj('requestService', {
|
||||||
configure: () => false,
|
configure: () => false,
|
||||||
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78'
|
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||||
|
getByHref: (uuid: string) => new RequestEntry()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
|
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||||
|
import { RestResponse } from '../../core/cache/response-cache.models';
|
||||||
|
|
||||||
export function getMockResponseCacheService(): ResponseCacheService {
|
export function getMockResponseCacheService(): ResponseCacheService {
|
||||||
return jasmine.createSpyObj('ResponseCacheService', [
|
return jasmine.createSpyObj('ResponseCacheService', {
|
||||||
'add',
|
add: (key: string, response: RestResponse, msToLive: number) => new ResponseCacheEntry(),
|
||||||
'get',
|
get: (key: string) => new ResponseCacheEntry(),
|
||||||
'has',
|
has: (key: string) => false,
|
||||||
]);
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator';
|
||||||
|
|
||||||
|
@searchResultFor(Collection)
|
||||||
export class CollectionSearchResult extends SearchResult<Collection> {
|
export class CollectionSearchResult extends SearchResult<Collection> {
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
|
||||||
import { Community } from '../../../core/shared/community.model';
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
|
import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator';
|
||||||
|
|
||||||
|
@searchResultFor(Community)
|
||||||
export class CommunitySearchResult extends SearchResult<Community> {
|
export class CommunitySearchResult extends SearchResult<Community> {
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { searchResultFor } from '../../../+search-page/search-service/search-result-element-decorator';
|
||||||
|
|
||||||
|
@searchResultFor(Item)
|
||||||
export class ItemSearchResult extends SearchResult<Item> {
|
export class ItemSearchResult extends SearchResult<Item> {
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<a [routerLink]="['/collections/' + object.id]"class="card-img-top">
|
<a [routerLink]="['/collections/', object.id]" class="card-img-top">
|
||||||
<ds-comcol-page-logo [logo]="object.logo">
|
<ds-grid-thumbnail [thumbnail]="object.logo">
|
||||||
</ds-comcol-page-logo>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{object.name}}</h4>
|
<h4 class="card-title">{{object.name}}</h4>
|
||||||
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a [routerLink]="['/collections/' + object.id]" class="lead btn btn-primary viewButton">View</a>
|
<a [routerLink]="['/collections/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<a [routerLink]="['/communities/' + object.id]"class="card-img-top">
|
<a [routerLink]="['/communities/', object.id]" class="card-img-top">
|
||||||
<ds-comcol-page-logo [logo]="object.logo">
|
<ds-grid-thumbnail [thumbnail]="object.logo">
|
||||||
</ds-comcol-page-logo>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{object.name}}</h4>
|
<h4 class="card-title">{{object.name}}</h4>
|
||||||
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a [routerLink]="['/communities/' + object.id]" class="lead btn btn-primary viewButton">View</a>
|
<a [routerLink]="['/communities/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<a [routerLink]="['/items/' + object.id]" class="card-img-top">
|
<a [routerLink]="['/items/', object.id]" class="card-img-top">
|
||||||
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
|
||||||
</ds-grid-thumbnail>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
|
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a [routerLink]="['/items/' + object.id]" class="lead btn btn-primary viewButton">View</a>
|
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,12 +10,14 @@
|
|||||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
(sortFieldChange)="onSortFieldChange($event)"
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
(paginationChange)="onPaginationChange($event)">
|
(paginationChange)="onPaginationChange($event)">
|
||||||
<div class="card-columns" *ngIf="objects?.hasSucceeded" @fadeIn>
|
<div class="card-columns row" *ngIf="objects?.hasSucceeded">
|
||||||
<div
|
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
|
||||||
*ngFor="let object of objects?.payload?.page">
|
<div class="card-element" *ngFor="let object of column">
|
||||||
<ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element>
|
<ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="objects.hasFailed | async" message="{{'error.objects' | translate}}"></ds-error>
|
<ds-error *ngIf="objects.hasFailed | async" message="{{'error.objects' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="objects.isLoading | async" message="{{'loading.objects' | translate}}"></ds-loading>
|
<ds-loading *ngIf="objects.isLoading | async" message="{{'loading.objects' | translate}}"></ds-loading>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
|
@@ -1,24 +1,26 @@
|
|||||||
@import '../../../styles/variables';
|
@import '../../../styles/variables';
|
||||||
@import '../../../styles/mixins';
|
@import '../../../styles/mixins';
|
||||||
|
|
||||||
|
$ds-wrapper-grid-spacing: $spacer/2;
|
||||||
|
|
||||||
ds-wrapper-grid-element ::ng-deep {
|
ds-wrapper-grid-element ::ng-deep {
|
||||||
div.thumbnail > img {
|
div.thumbnail > img {
|
||||||
height: $card-thumbnail-height;
|
height: $card-thumbnail-height;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
div.card {
|
div.card {
|
||||||
margin-bottom: $spacer;
|
margin-top: $ds-wrapper-grid-spacing;
|
||||||
|
margin-bottom: $ds-wrapper-grid-spacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-columns {
|
.card-columns {
|
||||||
@include media-breakpoint-only(lg) {
|
margin-left: -$ds-wrapper-grid-spacing;
|
||||||
column-count: 3;
|
margin-right: -$ds-wrapper-grid-spacing;
|
||||||
}
|
|
||||||
@include media-breakpoint-only(sm) {
|
.card-column {
|
||||||
column-count: 2;
|
padding-left: $ds-wrapper-grid-spacing;
|
||||||
}
|
padding-right: $ds-wrapper-grid-spacing;
|
||||||
@include media-breakpoint-only(xs) {
|
|
||||||
column-count: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,224 @@
|
|||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { WidthCategory } from '../host-window.service';
|
||||||
|
import { ObjectGridComponent } from './object-grid.component';
|
||||||
|
|
||||||
|
describe('ObjectGridComponent', () => {
|
||||||
|
const testObjects = [
|
||||||
|
{ one: 1 },
|
||||||
|
{ two: 2 },
|
||||||
|
{ three: 3 },
|
||||||
|
{ four: 4 },
|
||||||
|
{ five: 5 },
|
||||||
|
{ six: 6 },
|
||||||
|
{ seven: 7 },
|
||||||
|
{ eight: 8 },
|
||||||
|
{ nine: 9 },
|
||||||
|
{ ten: 10 }
|
||||||
|
];
|
||||||
|
const mockRD = {
|
||||||
|
payload: {
|
||||||
|
page: testObjects
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
describe('the number of columns', () => {
|
||||||
|
|
||||||
|
it('should be 3 for xl screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.XL }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: 3 });
|
||||||
|
|
||||||
|
const result = comp.columns$.pipe(
|
||||||
|
map((columns) => columns.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be 3 for lg screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.LG }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: 3 });
|
||||||
|
|
||||||
|
const result = comp.columns$.pipe(
|
||||||
|
map((columns) => columns.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be 2 for md screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.MD }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: 2 });
|
||||||
|
|
||||||
|
const result = comp.columns$.pipe(
|
||||||
|
map((columns) => columns.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be 2 for sm screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.SM }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: 2 });
|
||||||
|
|
||||||
|
const result = comp.columns$.pipe(
|
||||||
|
map((columns) => columns.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be 1 for xs screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.XS }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: 1 });
|
||||||
|
|
||||||
|
const result = comp.columns$.pipe(
|
||||||
|
map((columns) => columns.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The ordering of the content', () => {
|
||||||
|
it('should be left to right for XL screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.XL }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: [
|
||||||
|
[testObjects[0], testObjects[3], testObjects[6], testObjects[9]],
|
||||||
|
[testObjects[1], testObjects[4], testObjects[7]],
|
||||||
|
[testObjects[2], testObjects[5], testObjects[8]]
|
||||||
|
] });
|
||||||
|
|
||||||
|
const result = comp.columns$;
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be left to right for LG screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.LG }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: [
|
||||||
|
[testObjects[0], testObjects[3], testObjects[6], testObjects[9]],
|
||||||
|
[testObjects[1], testObjects[4], testObjects[7]],
|
||||||
|
[testObjects[2], testObjects[5], testObjects[8]]
|
||||||
|
] });
|
||||||
|
|
||||||
|
const result = comp.columns$;
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be left to right for MD screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.MD }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: [
|
||||||
|
[testObjects[0], testObjects[2], testObjects[4], testObjects[6], testObjects[8]],
|
||||||
|
[testObjects[1], testObjects[3], testObjects[5], testObjects[7], testObjects[9]],
|
||||||
|
] });
|
||||||
|
|
||||||
|
const result = comp.columns$;
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be left to right for SM screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.SM }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: [
|
||||||
|
[testObjects[0], testObjects[2], testObjects[4], testObjects[6], testObjects[8]],
|
||||||
|
[testObjects[1], testObjects[3], testObjects[5], testObjects[7], testObjects[9]],
|
||||||
|
] });
|
||||||
|
|
||||||
|
const result = comp.columns$;
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be top to bottom for XS screens', () => {
|
||||||
|
const hostWindowService = {
|
||||||
|
widthCategory: hot('a', { a: WidthCategory.XS }),
|
||||||
|
} as any;
|
||||||
|
const comp = new ObjectGridComponent(hostWindowService);
|
||||||
|
|
||||||
|
(comp as any)._objects$ = hot('b', { b: mockRD });
|
||||||
|
|
||||||
|
comp.ngOnInit();
|
||||||
|
|
||||||
|
const expected = cold('c', { c: [ testObjects ] });
|
||||||
|
|
||||||
|
const result = comp.columns$;
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -2,16 +2,21 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input, OnInit,
|
||||||
Output,
|
Output,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { fadeIn } from '../animations/fade';
|
import { fadeIn } from '../animations/fade';
|
||||||
|
import { hasNoValue, hasValue } from '../empty.util';
|
||||||
|
import { HostWindowService, WidthCategory } from '../host-window.service';
|
||||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
@@ -25,18 +30,18 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
|
|||||||
animations: [fadeIn]
|
animations: [fadeIn]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ObjectGridComponent {
|
export class ObjectGridComponent implements OnInit {
|
||||||
|
|
||||||
@Input() config: PaginationComponentOptions;
|
@Input() config: PaginationComponentOptions;
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
@Input() hideGear = false;
|
@Input() hideGear = false;
|
||||||
@Input() hidePagerWhenSinglePage = true;
|
@Input() hidePagerWhenSinglePage = true;
|
||||||
private _objects: RemoteData<PaginatedList<ListableObject>>;
|
private _objects$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>;
|
||||||
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
|
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
|
||||||
this._objects = objects;
|
this._objects$.next(objects);
|
||||||
}
|
}
|
||||||
get objects() {
|
get objects() {
|
||||||
return this._objects;
|
return this._objects$.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +82,56 @@ export class ObjectGridComponent {
|
|||||||
*/
|
*/
|
||||||
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
data: any = {};
|
data: any = {};
|
||||||
|
columns$: Observable<ListableObject[]>
|
||||||
|
|
||||||
|
constructor(private hostWindow: HostWindowService) {
|
||||||
|
this._objects$ = new BehaviorSubject(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const nbColumns$ = this.hostWindow.widthCategory.pipe(
|
||||||
|
map((widthCat: WidthCategory) => {
|
||||||
|
switch (widthCat) {
|
||||||
|
case WidthCategory.XL:
|
||||||
|
case WidthCategory.LG: {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
case WidthCategory.MD:
|
||||||
|
case WidthCategory.SM: {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
distinctUntilChanged()
|
||||||
|
).startWith(3);
|
||||||
|
|
||||||
|
this.columns$ = Observable.combineLatest(
|
||||||
|
nbColumns$,
|
||||||
|
this._objects$,
|
||||||
|
(nbColumns, objects) => {
|
||||||
|
if (hasValue(objects) && hasValue(objects.payload) && hasValue(objects.payload.page)) {
|
||||||
|
const page = objects.payload.page;
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
page.forEach((obj: ListableObject, i: number) => {
|
||||||
|
const colNb = i % nbColumns;
|
||||||
|
let col = result[colNb];
|
||||||
|
if (hasNoValue(col)) {
|
||||||
|
col = [];
|
||||||
|
}
|
||||||
|
result[colNb] = [...col, obj];
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.pageChange.emit(event);
|
this.pageChange.emit(event);
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<a [routerLink]="['/collections/' + dso.id]"class="card-img-top">
|
<a [routerLink]="['/collections/', dso.id]" class="card-img-top">
|
||||||
<ds-comcol-page-logo [logo]="dso.logo">
|
<ds-grid-thumbnail [thumbnail]="dso.logo">
|
||||||
</ds-comcol-page-logo>
|
</ds-grid-thumbnail>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{dso.name}}</h4>
|
<h4 class="card-title">{{dso.name}}</h4>
|
||||||
<p *ngIf="dso.shortDescription" class="card-text">{{dso.shortDescription}}</p>
|
<p *ngIf="dso.shortDescription" class="card-text">{{dso.shortDescription}}</p>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a [routerLink]="['/collections/' + dso.id]" class="lead btn btn-primary viewButton">View</a>
|
<a [routerLink]="['/collections/', dso.id]" class="lead btn btn-primary viewButton">View</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user