@@ -28,6 +30,8 @@
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
+ [searchOptions]="(searchOptions$ | async)"
+ [sortOptions]="(sortOptions$ | async)"
[refreshFilters]="refreshFilters.asObservable()"
[inPlaceSearch]="inPlaceSearch">
diff --git a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts
index 59581d0da8..909239b61c 100644
--- a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts
+++ b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts
@@ -45,6 +45,7 @@ describe('MyDSpacePageComponent', () => {
pagination.id = 'mydspace-results-pagination';
pagination.currentPage = 1;
pagination.pageSize = 10;
+ const sortOption = { name: 'score', metadata: null };
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
const searchServiceStub = jasmine.createSpyObj('SearchService', {
@@ -52,7 +53,8 @@ describe('MyDSpacePageComponent', () => {
getEndpoint: observableOf('discover/search/objects'),
getSearchLink: '/mydspace',
getScopes: observableOf(['test-scope']),
- setServiceOptions: {}
+ setServiceOptions: {},
+ getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
});
const configurationParam = 'default';
const queryParam = 'test query';
@@ -188,4 +190,24 @@ describe('MyDSpacePageComponent', () => {
});
});
+
+ describe('when stable', () => {
+
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should have initialized the sortOptions$ observable', (done) => {
+
+ comp.sortOptions$.subscribe((sortOptions) => {
+
+ expect(sortOptions.length).toEqual(2);
+ expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC));
+ expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC));
+ done();
+ });
+
+ });
+
+ });
});
diff --git a/src/app/+my-dspace-page/my-dspace-page.component.ts b/src/app/+my-dspace-page/my-dspace-page.component.ts
index 5ee2a47d9f..ba5c0e5cc7 100644
--- a/src/app/+my-dspace-page/my-dspace-page.component.ts
+++ b/src/app/+my-dspace-page/my-dspace-page.component.ts
@@ -7,8 +7,8 @@ import {
OnInit
} from '@angular/core';
-import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
-import { map, switchMap, tap, } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
+import { map, switchMap, take, tap } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data';
@@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option
import { SearchService } from '../core/shared/search/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util';
-import { getFirstSucceededRemoteData } from '../core/shared/operators';
+import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types';
@@ -29,6 +29,8 @@ import { ViewMode } from '../core/shared/view-mode.model';
import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../shared/search/search-result.model';
import { Context } from '../core/shared/context.model';
+import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
+import { SearchConfig } from '../core/shared/search/search-filters/search-config.model';
export const MYDSPACE_ROUTE = '/mydspace';
export const SEARCH_CONFIG_SERVICE: InjectionToken = new InjectionToken('searchConfigurationService');
@@ -71,6 +73,11 @@ export class MyDSpacePageComponent implements OnInit {
*/
searchOptions$: Observable;
+ /**
+ * The current available sort options
+ */
+ sortOptions$: Observable;
+
/**
* The current relevant scopes
*/
@@ -151,6 +158,27 @@ export class MyDSpacePageComponent implements OnInit {
})
);
+ this.sortOptions$ = this.context$.pipe(
+ switchMap((context) => this.service.getSearchConfigurationFor(null, context)),
+ getFirstSucceededRemoteDataPayload(),
+ map((searchConfig: SearchConfig) => {
+ const sortOptions = [];
+ searchConfig.sortOptions.forEach(sortOption => {
+ sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC));
+ sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC));
+ });
+ return sortOptions;
+ }));
+
+ combineLatest([
+ this.sortOptions$,
+ this.searchConfigService.paginatedSearchOptions
+ ]).pipe(take(1))
+ .subscribe(([sortOptions, searchOptions]) => {
+ const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]});
+ this.searchConfigService.paginatedSearchOptions.next(updateValue);
+ });
+
}
/**
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index f73bfd0bdf..62265fdedf 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -162,6 +162,7 @@ import { ShortLivedToken } from './auth/models/short-lived-token.model';
import { UsageReport } from './statistics/models/usage-report.model';
import { RootDataService } from './data/root-data.service';
import { Root } from './data/root.model';
+import { SearchConfig } from './shared/search/search-filters/search-config.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -342,6 +343,7 @@ export const models =
Registration,
UsageReport,
Root,
+ SearchConfig
];
@NgModule({
diff --git a/src/app/core/shared/search/search-filters/search-config.model.ts b/src/app/core/shared/search/search-filters/search-config.model.ts
new file mode 100644
index 0000000000..dd7a799f37
--- /dev/null
+++ b/src/app/core/shared/search/search-filters/search-config.model.ts
@@ -0,0 +1,75 @@
+import { autoserialize, deserialize } from 'cerialize';
+
+import { SEARCH_CONFIG } from './search-config.resource-type';
+import { typedObject } from '../../../cache/builders/build-decorators';
+import { CacheableObject } from '../../../cache/object-cache.reducer';
+import { HALLink } from '../../hal-link.model';
+import { ResourceType } from '../../resource-type';
+
+/**
+ * The configuration for a search
+ */
+@typedObject
+export class SearchConfig implements CacheableObject {
+ static type = SEARCH_CONFIG;
+
+ /**
+ * The id of this search configuration.
+ */
+ @autoserialize
+ id: string;
+
+ /**
+ * The configured filters.
+ */
+ @autoserialize
+ filters: FilterConfig[];
+
+ /**
+ * The configured sort options.
+ */
+ @autoserialize
+ sortOptions: SortOption[];
+
+ /**
+ * The object type.
+ */
+ @autoserialize
+ type: ResourceType;
+
+ /**
+ * The {@link HALLink}s for this Item
+ */
+ @deserialize
+ _links: {
+ facets: HALLink;
+ objects: HALLink;
+ self: HALLink;
+ };
+}
+
+/**
+ * Interface to model filter's configuration.
+ */
+export interface FilterConfig {
+ filter: string;
+ hasFacets: boolean;
+ operators: OperatorConfig[];
+ openByDefault: boolean;
+ pageSize: number;
+ type: string;
+}
+
+/**
+ * Interface to model sort option's configuration.
+ */
+export interface SortOption {
+ name: string;
+}
+
+/**
+ * Interface to model operator's configuration.
+ */
+export interface OperatorConfig {
+ operator: string;
+}
diff --git a/src/app/core/shared/search/search-filters/search-config.resource-type.ts b/src/app/core/shared/search/search-filters/search-config.resource-type.ts
new file mode 100644
index 0000000000..967a654006
--- /dev/null
+++ b/src/app/core/shared/search/search-filters/search-config.resource-type.ts
@@ -0,0 +1,9 @@
+import {ResourceType} from '../../resource-type';
+
+/**
+ * The resource type for SearchConfig
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const SEARCH_CONFIG = new ResourceType('discover');
diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts
index 06208094bd..d09444ad6c 100644
--- a/src/app/core/shared/search/search.service.spec.ts
+++ b/src/app/core/shared/search/search.service.spec.ts
@@ -230,5 +230,55 @@ describe('SearchService', () => {
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
});
});
+
+ describe('when getSearchConfigurationFor is called without a scope', () => {
+ const endPoint = 'http://endpoint.com/test/config';
+ beforeEach(() => {
+ spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
+ spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough();
+ /* tslint:disable:no-empty */
+ searchService.getSearchConfigurationFor(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.send).toHaveBeenCalled();
+ });
+
+ it('should call send containing a request with the correct request url', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: endPoint }), true);
+ });
+ });
+
+ describe('when getSearchConfigurationFor is called with a scope', () => {
+ const endPoint = 'http://endpoint.com/test/config';
+ const scope = 'test';
+ const requestUrl = endPoint + '?scope=' + scope;
+ beforeEach(() => {
+ spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
+ /* tslint:disable:no-empty */
+ searchService.getSearchConfigurationFor(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.send).toHaveBeenCalled();
+ });
+
+ it('should call send containing a request with the correct request url', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
+ });
+ });
+
});
});
diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts
index b380a70d44..7747717830 100644
--- a/src/app/core/shared/search/search.service.ts
+++ b/src/app/core/shared/search/search.service.ts
@@ -37,12 +37,19 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
import { FacetValues } from '../../../shared/search/facet-values.model';
+import { SearchConfig } from './search-filters/search-config.model';
/**
* Service that performs all general actions that have to do with the search page
*/
@Injectable()
export class SearchService implements OnDestroy {
+
+ /**
+ * Endpoint link path for retrieving search configurations
+ */
+ private configurationLinkPath = 'discover/search';
+
/**
* Endpoint link path for retrieving general search results
*/
@@ -224,6 +231,24 @@ export class SearchService implements OnDestroy {
);
}
+ private getConfigUrl(url: string, scope?: string, configurationName?: string) {
+ const args: string[] = [];
+
+ if (isNotEmpty(scope)) {
+ args.push(`scope=${scope}`);
+ }
+
+ if (isNotEmpty(configurationName)) {
+ args.push(`configuration=${configurationName}`);
+ }
+
+ if (isNotEmpty(args)) {
+ url = new URLCombiner(url, `?${args.join('&')}`).toString();
+ }
+
+ return url;
+ }
+
/**
* Request the filter configuration for a given scope or the whole repository
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
@@ -232,33 +257,17 @@ export class SearchService implements OnDestroy {
*/
getConfig(scope?: string, configurationName?: string): Observable> {
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
- map((url: string) => {
- const args: string[] = [];
-
- if (isNotEmpty(scope)) {
- args.push(`scope=${scope}`);
- }
-
- if (isNotEmpty(configurationName)) {
- args.push(`configuration=${configurationName}`);
- }
-
- if (isNotEmpty(args)) {
- url = new URLCombiner(url, `?${args.join('&')}`).toString();
- }
-
- return url;
- }),
+ map((url: string) => this.getConfigUrl(url, scope, configurationName)),
);
href$.pipe(take(1)).subscribe((url: string) => {
- let request = new this.request(this.requestService.generateRequestId(), url);
- request = Object.assign(request, {
- getResponseParser(): GenericConstructor {
- return FacetConfigResponseParsingService;
- }
- });
- this.requestService.send(request, true);
+ let request = new this.request(this.requestService.generateRequestId(), url);
+ request = Object.assign(request, {
+ getResponseParser(): GenericConstructor {
+ return FacetConfigResponseParsingService;
+ }
+ });
+ this.requestService.send(request, true);
});
return this.rdb.buildFromHref(href$).pipe(
@@ -397,6 +406,25 @@ export class SearchService implements OnDestroy {
});
}
+ /**
+ * Request the search configuration for a given scope or the whole repository
+ * @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
+ * @param {string} configurationName the name of the configuration
+ * @returns {Observable>} The found configuration
+ */
+ getSearchConfigurationFor(scope?: string, configurationName?: string ): Observable> {
+ const href$ = this.halService.getEndpoint(this.configurationLinkPath).pipe(
+ map((url: string) => this.getConfigUrl(url, scope, configurationName)),
+ );
+
+ href$.pipe(take(1)).subscribe((url: string) => {
+ const request = new this.request(this.requestService.generateRequestId(), url);
+ this.requestService.send(request, true);
+ });
+
+ return this.rdb.buildFromHref(href$);
+ }
+
/**
* @returns {string} The base path to the search page
*/
diff --git a/src/app/shared/search/search-settings/search-settings.component.html b/src/app/shared/search/search-settings/search-settings.component.html
index f6806e6843..a31678743d 100644
--- a/src/app/shared/search/search-settings/search-settings.component.html
+++ b/src/app/shared/search/search-settings/search-settings.component.html
@@ -1,4 +1,4 @@
-
+
{{ 'search.sidebar.settings.title' | translate}}
-
-
\ No newline at end of file
+
diff --git a/src/app/shared/search/search-settings/search-settings.component.spec.ts b/src/app/shared/search/search-settings/search-settings.component.spec.ts
index cd4a872815..221e3a0dea 100644
--- a/src/app/shared/search/search-settings/search-settings.component.spec.ts
+++ b/src/app/shared/search/search-settings/search-settings.component.spec.ts
@@ -12,7 +12,6 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
import { By } from '@angular/platform-browser';
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
import { VarDirective } from '../../utils/var.directive';
-import { take } from 'rxjs/operators';
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
import { SidebarService } from '../../sidebar/sidebar.service';
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
@@ -81,7 +80,7 @@ describe('SearchSettingsComponent', () => {
provide: SEARCH_CONFIG_SERVICE,
useValue: {
paginatedSearchOptions: observableOf(paginatedSearchOptions),
- getCurrentScope: observableOf('test-id')
+ getCurrentScope: observableOf('test-id'),
}
},
],
@@ -93,6 +92,14 @@ describe('SearchSettingsComponent', () => {
fixture = TestBed.createComponent(SearchSettingsComponent);
comp = fixture.componentInstance;
+ comp.sortOptions = [
+ new SortOptions('score', SortDirection.DESC),
+ new SortOptions('dc.title', SortDirection.ASC),
+ new SortOptions('dc.title', SortDirection.DESC)
+ ];
+
+ comp.searchOptions = paginatedSearchOptions;
+
// SearchPageComponent test instance
fixture.detectChanges();
searchServiceObject = (comp as any).service;
@@ -101,34 +108,24 @@ describe('SearchSettingsComponent', () => {
});
- it('it should show the order settings with the respective selectable options', (done) => {
- (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
- fixture.detectChanges();
- const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
- expect(orderSetting).toBeDefined();
- const childElements = orderSetting.queryAll(By.css('option'));
- expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
- done();
- });
+ it('it should show the order settings with the respective selectable options', () => {
+ fixture.detectChanges();
+ const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
+ expect(orderSetting).toBeDefined();
+ const childElements = orderSetting.queryAll(By.css('option'));
+ expect(childElements.length).toEqual(comp.sortOptions.length);
});
- it('it should show the size settings', (done) => {
- (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
- fixture.detectChanges();
- const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
- expect(pageSizeSetting).toBeDefined();
- done();
- }
- );
+ it('it should show the size settings', () => {
+ fixture.detectChanges();
+ const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
+ expect(pageSizeSetting).toBeDefined();
});
- it('should have the proper order value selected by default', (done) => {
- (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
- fixture.detectChanges();
- const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
- const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
- expect(childElementToBeSelected).toBeDefined();
- done();
- });
+ it('should have the proper order value selected by default', () => {
+ fixture.detectChanges();
+ const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
+ const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]'));
+ expect(childElementToBeSelected).toBeDefined();
});
});
diff --git a/src/app/shared/search/search-settings/search-settings.component.ts b/src/app/shared/search/search-settings/search-settings.component.ts
index 45d7c7b432..d85f234aa3 100644
--- a/src/app/shared/search/search-settings/search-settings.component.ts
+++ b/src/app/shared/search/search-settings/search-settings.component.ts
@@ -1,9 +1,8 @@
-import { Component, Inject, OnInit } from '@angular/core';
+import { Component, Inject, Input } from '@angular/core';
import { SearchService } from '../../../core/shared/search/search.service';
-import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
+import { SortOptions } from '../../../core/cache/models/sort-options.model';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
-import { Observable } from 'rxjs';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
@@ -16,16 +15,17 @@ import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.c
/**
* This component represents the part of the search sidebar that contains the general search settings.
*/
-export class SearchSettingsComponent implements OnInit {
+export class SearchSettingsComponent {
+
/**
* The configuration for the current paginated search results
*/
- searchOptions$: Observable;
+ @Input() searchOptions: PaginatedSearchOptions;
/**
* All sort options that are shown in the settings
*/
- searchOptionPossibilities = [new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC)];
+ @Input() sortOptions: SortOptions[];
constructor(private service: SearchService,
private route: ActivatedRoute,
@@ -33,13 +33,6 @@ export class SearchSettingsComponent implements OnInit {
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
}
- /**
- * Initialize paginated search options
- */
- ngOnInit(): void {
- this.searchOptions$ = this.searchConfigurationService.paginatedSearchOptions;
- }
-
/**
* Method to change the current sort field and direction
* @param {Event} event Change event containing the sort direction and sort field
diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.html b/src/app/shared/search/search-sidebar/search-sidebar.component.html
index 74abeadfd8..624d094d22 100644
--- a/src/app/shared/search/search-sidebar/search-sidebar.component.html
+++ b/src/app/shared/search/search-sidebar/search-sidebar.component.html
@@ -12,7 +12,7 @@
-
+
diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.ts b/src/app/shared/search/search-sidebar/search-sidebar.component.ts
index 2060e0f345..7f134cacd3 100644
--- a/src/app/shared/search/search-sidebar/search-sidebar.component.ts
+++ b/src/app/shared/search/search-sidebar/search-sidebar.component.ts
@@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
import { Observable } from 'rxjs';
+import { PaginatedSearchOptions } from '../paginated-search-options.model';
+import { SortOptions } from '../../../core/cache/models/sort-options.model';
/**
* This component renders a simple item page.
@@ -45,6 +47,16 @@ export class SearchSidebarComponent {
*/
@Input() inPlaceSearch;
+ /**
+ * The configuration for the current paginated search results
+ */
+ @Input() searchOptions: PaginatedSearchOptions;
+
+ /**
+ * All sort options that are shown in the settings
+ */
+ @Input() sortOptions: SortOptions[];
+
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/