added more documentation and cleaned everything up a bit

This commit is contained in:
lotte
2018-07-27 14:09:02 +02:00
parent 04f9e5e312
commit 1ab53e7204
15 changed files with 261 additions and 65 deletions

View File

@@ -1,12 +1,15 @@
import { FilterType } from '../../search-service/filter-type.model';
/**
* Contains the mapping between a facet component and a FilterType
*/
const filterTypeMap = new Map();
/**
* Sets the mapping for a component in relation to a filter type
* Sets the mapping for a facet component in relation to a filter type
* @param {FilterType} type The type for which the matching component is mapped
* @returns Decorator function that performs the actual mapping on initialization of the component
* @returns Decorator function that performs the actual mapping on initialization of the facet component
*/
export function renderFacetFor(type: FilterType) {
return function decorator(objectElement: any) {
@@ -18,9 +21,9 @@ export function renderFacetFor(type: FilterType) {
}
/**
* Requests the matching component based on a given filter type
* @param {FilterType} type The filter type for which the component is requested
* @returns The component's constructor that matches the given filter type
* Requests the matching facet component based on a given filter type
* @param {FilterType} type The filter type for which the facet component is requested
* @returns The facet component's constructor that matches the given filter type
*/
export function renderFilterType(type: FilterType) {
return filterTypeMap.get(type);

View File

@@ -26,6 +26,9 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
/**
* Service that performs all actions that have to do with search filters and facets
*/
@Injectable()
export class SearchFilterService {

View File

@@ -35,13 +35,30 @@ import { hasValue } from '../shared/empty.util';
*/
export class SearchPageComponent implements OnInit {
/**
* The current search results
*/
resultsRD$: Subject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new Subject();
/**
* The current paginated search options
*/
searchOptions$: Observable<PaginatedSearchOptions>;
sortConfig: SortOptions;
/**
* The current relevant scopes
*/
scopeListRD$: Observable<DSpaceObject[]>;
/**
* Emits true if were on a small screen
*/
isXsOrSm$: Observable<boolean>;
pageSize;
pageSizeOptions;
/**
* Default values for the Search Options
*/
defaults = {
pagination: {
id: 'search-results-pagination',
@@ -51,6 +68,10 @@ export class SearchPageComponent implements OnInit {
query: '',
scope: ''
};
/**
* Subscription to unsubscribe from
*/
sub: Subscription;
constructor(private service: SearchService,
@@ -93,7 +114,7 @@ export class SearchPageComponent implements OnInit {
}
/**
* Check if the sidebar is correct
* Check if the sidebar is collapsed
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
*/
public isSidebarCollapsed(): Observable<boolean> {
@@ -107,6 +128,9 @@ export class SearchPageComponent implements OnInit {
return this.service.getSearchLink();
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();

View File

@@ -2,19 +2,31 @@
import { autoserialize, autoserializeAs } from 'cerialize';
/**
*
* The configuration for a search filter
*/
export class SearchFilterConfig {
/**
* The name of this filter
*/
@autoserialize
name: string;
/**
* The FilterType of this filter
*/
@autoserializeAs(String, 'facetType')
type: FilterType;
/**
* True if the filter has facets
*/
@autoserialize
hasFacets: boolean;
/**
* @type {number} The page size used for this facet
*/
@autoserializeAs(String, 'facetLimit')
pageSize = 5;
@@ -35,6 +47,7 @@
*/
@autoserialize
minValue: string;
/**
* Name of this configuration that can be used in a url
* @returns Parameter name

View File

@@ -2,46 +2,88 @@ import { autoserialize, autoserializeAs } from 'cerialize';
import { PageInfo } from '../../core/shared/page-info.model';
import { NormalizedSearchResult } from '../normalized-search-result.model';
/**
* Class representing the response returned by the server when performing a search request
*/
export class SearchQueryResponse {
/**
* The scope used in the search request represented by the UUID of a DSpaceObject
*/
@autoserialize
scope: string;
/**
* The search query used in the search request
*/
@autoserialize
query: string;
/**
* The currently active filters used in the search request
*/
@autoserialize
appliedFilters: any[]; // TODO
/**
* The sort parameters used in the search request
*/
@autoserialize
sort: any; // TODO
/**
* The sort parameters used in the search request
*/
@autoserialize
configurationName: string;
/**
* The sort parameters used in the search request
*/
@autoserialize
public type: string;
/**
* Pagination configuration for this response
*/
@autoserialize
page: PageInfo;
/**
* The results for this query
*/
@autoserializeAs(NormalizedSearchResult)
objects: NormalizedSearchResult[];
@autoserialize
facets: any; // TODO
/**
* The REST url to retrieve the current response
*/
@autoserialize
self: string;
/**
* The REST url to retrieve the next response
*/
@autoserialize
next: string;
/**
* The REST url to retrieve the previous response
*/
@autoserialize
previous: string;
/**
* The REST url to retrieve the first response
*/
@autoserialize
first: string;
/**
* The REST url to retrieve the last response
*/
@autoserialize
last: string;
}

View File

@@ -1,8 +1,16 @@
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
/**
* Contains the mapping between a search result component and a DSpaceObject
*/
const searchResultMap = new Map();
/**
* Used to map Search Result components to their matching DSpaceObject
* @param {GenericConstructor<ListableObject>} domainConstructor The constructor of the DSpaceObject
* @returns Decorator function that performs the actual mapping on initialization of the component
*/
export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
return function decorator(searchResult: any) {
if (!searchResult) {
@@ -12,6 +20,11 @@ export function searchResultFor(domainConstructor: GenericConstructor<ListableOb
};
}
/**
* Requests the matching component based on a given DSpaceObject's constructor
* @param {GenericConstructor<ListableObject>} domainConstructor The DSpaceObject's constructor for which the search result component is requested
* @returns The component's constructor that matches the given DSpaceObject
*/
export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
return searchResultMap.get(domainConstructor);
}

View File

@@ -45,16 +45,27 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.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 general search results
*/
private searchLinkPath = 'discover/search/objects';
private facetValueLinkPathPrefix = 'discover/facets/';
private facetConfigLinkPath = 'discover/facets';
/**
* Endpoint link path for retrieving facet config incl values
*/
private facetLinkPathPrefix = 'discover/facets/';
/**
* Subscription to unsubscribe from
*/
private sub;
searchOptions: SearchOptions;
constructor(private router: Router,
private route: ActivatedRoute,
protected responseCache: ResponseCacheService,
@@ -63,14 +74,13 @@ export class SearchService implements OnDestroy {
private halService: HALEndpointService,
private communityService: CommunityDataService,
private collectionService: CollectionDataService) {
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
pagination.id = 'search-results-pagination';
pagination.currentPage = 1;
pagination.pageSize = 10;
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort });
}
/**
* Method to retrieve a paginated list of search results from the server
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
*/
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url: string) => {
@@ -139,8 +149,13 @@ export class SearchService implements OnDestroy {
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
}
/**
* 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
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
*/
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
const requestObs = this.halService.getEndpoint(this.facetConfigLinkPath).pipe(
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
map((url: string) => {
const args: string[] = [];
@@ -180,12 +195,19 @@ export class SearchService implements OnDestroy {
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
}
/**
* Method to request a single page of filter values for a given value
* @param {SearchFilterConfig} filterConfig The filter config for which we want to request filter values
* @param {number} valuePage The page number of the filter values
* @param {SearchOptions} searchOptions The search configuration for the current search
* @param {string} filterQuery The optional query used to filter out filter values
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
*/
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<PaginatedList<FacetValue>>> {
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
const requestObs = this.halService.getEndpoint(this.facetLinkPathPrefix + filterConfig.name).pipe(
map((url: string) => {
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
if (hasValue(filterQuery)) {
// args.push(`${filterConfig.paramName}=${filterQuery},query`);
args.push(`prefix=${filterQuery}`);
}
if (hasValue(searchOptions)) {
@@ -228,7 +250,12 @@ export class SearchService implements OnDestroy {
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
}
getScopes(scopeId: string): Observable<DSpaceObject[]> {
/**
* Request a list of DSpaceObjects that can be used as a scope, based on the current scope
* @param {string} scopeId UUID of the current scope, if the scope is empty, the repository wide scopes will be returned
* @returns {Observable<DSpaceObject[]>} Emits a list of DSpaceObjects which represent possible scopes
*/
getScopes(scopeId?: string): Observable<DSpaceObject[]> {
if (hasNoValue(scopeId)) {
const top: Observable<Community[]> = this.communityService.findTop({ elementsPerPage: 9999 }).pipe(
@@ -260,6 +287,10 @@ export class SearchService implements OnDestroy {
}
/**
* Requests the current view mode based on the current URL
* @returns {Observable<ViewMode>} The current view mode
*/
getViewMode(): Observable<ViewMode> {
return this.route.queryParams.map((params) => {
if (isNotEmpty(params.view) && hasValue(params.view)) {
@@ -270,6 +301,10 @@ export class SearchService implements OnDestroy {
});
}
/**
* Changes the current view mode in the current URL
* @param {ViewMode} viewMode Mode to switch to
*/
setViewMode(viewMode: ViewMode) {
const navigationExtras: NavigationExtras = {
queryParams: { view: viewMode },
@@ -279,12 +314,18 @@ export class SearchService implements OnDestroy {
this.router.navigate([this.getSearchLink()], navigationExtras);
}
/**
* @returns {string} The base path to the search page
*/
getSearchLink(): string {
const urlTree = this.router.parseUrl(this.router.url);
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
return '/' + g.toString();
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
if (this.sub !== undefined) {
this.sub.unsubscribe();

View File

@@ -1,22 +1,24 @@
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div *ngIf="[searchOptions].sort" class="setting-option result-order-settings mb-3 p-3">
<ng-container *ngVar="(searchOptions$ | async) as config">
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div *ngIf="config?.sort" class="setting-option result-order-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
<select class="form-control" (change)="reloadOrder($event)">
<option *ngFor="let sortOption of searchOptionPossibilities"
[value]="sortOption.field + ',' + sortOption.direction.toString()"
[selected]="sortOption.field === field && sortOption.direction === direction? 'selected': null">
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</select>
</div>
</div>
<div class="setting-option page-size-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
<select class="form-control" (change)="reloadRPP($event)">
<option *ngFor="let pageSizeOption of pageSizeOptions" [value]="pageSizeOption"
[selected]="pageSizeOption === pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</select>
</div>
<div class="setting-option page-size-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
<select class="form-control" (change)="reloadRPP($event)">
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
[value]="pageSizeOption"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</select>
</div>
</ng-container>

View File

@@ -1,34 +1,35 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'ds-search-settings',
styleUrls: ['./search-settings.component.scss'],
templateUrl: './search-settings.component.html'
})
/**
* This component represents the part of the search sidebar that contains the general search settings.
*/
export class SearchSettingsComponent implements OnInit {
@Input() searchOptions: PaginatedSearchOptions;
/**
* Declare SortDirection enumeration to use it in the template
* The configuration for the current paginated search results
*/
public sortDirections = SortDirection;
/**
* Number of items per page.
*/
public pageSize;
@Input() public pageSizeOptions;
searchOptions$: Observable<PaginatedSearchOptions>;
query: string;
page: number;
direction: SortDirection;
field: string;
currentParams = {};
/**
* 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)];
/**
* Default values for the Search Options
*/
defaults = {
pagination: {
id: 'search-results-pagination',
@@ -38,22 +39,24 @@ export class SearchSettingsComponent implements OnInit {
query: '',
scope: ''
};
constructor(private service: SearchService,
private route: ActivatedRoute,
private router: Router,
private filterService: SearchFilterService) {
}
/**
* Initialize paginated search options
*/
ngOnInit(): void {
this.filterService.getPaginatedSearchOptions(this.defaults).subscribe((options) => {
this.direction = options.sort.direction;
this.field = options.sort.field;
this.searchOptions = options;
this.pageSize = options.pagination.pageSize;
this.pageSizeOptions = options.pagination.pageSizeOptions
})
this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults);
}
/**
* Method to change the current page size (results per page)
* @param {Event} event Change event containing the new page size value
*/
reloadRPP(event: Event) {
const value = (event.target as HTMLInputElement).value;
const navigationExtras: NavigationExtras = {
@@ -65,6 +68,10 @@ export class SearchSettingsComponent implements OnInit {
this.router.navigate([ '/search' ], navigationExtras);
}
/**
* Method to change the current sort field and direction
* @param {Event} event Change event containing the sort direction and sort field
*/
reloadOrder(event: Event) {
const values = (event.target as HTMLInputElement).value.split(',');
const navigationExtras: NavigationExtras = {

View File

@@ -17,14 +17,23 @@ export const SearchSidebarActionTypes = {
};
/* tslint:disable:max-classes-per-file */
/**
* Used to collapse the sidebar
*/
export class SearchSidebarCollapseAction implements Action {
type = SearchSidebarActionTypes.COLLAPSE;
}
/**
* Used to expand the sidebar
*/
export class SearchSidebarExpandAction implements Action {
type = SearchSidebarActionTypes.EXPAND;
}
/**
* Used to collapse the sidebar when it's expanded and expand it when it's collapsed
*/
export class SearchSidebarToggleAction implements Action {
type = SearchSidebarActionTypes.TOGGLE;
}

View File

@@ -12,7 +12,18 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
templateUrl: './search-sidebar.component.html',
})
/**
* Component representing the sidebar on the search page
*/
export class SearchSidebarComponent {
/**
* The total amount of result
*/
@Input() resultCount;
/**
* Emits event when the user clicks a button to open or close the sidebar
*/
@Output() toggleSidebar = new EventEmitter<boolean>();
}

View File

@@ -5,6 +5,9 @@ import * as fromRouter from '@ngrx/router-store';
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
import { URLBaser } from '../../core/url-baser/url-baser';
/**
* Makes sure that if the user navigates to another route, the sidebar is collapsed
*/
@Injectable()
export class SearchSidebarEffects {
private previousPath: string;

View File

@@ -1,5 +1,8 @@
import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
/**
* Interface that represents the state of the sidebar
*/
export interface SearchSidebarState {
sidebarCollapsed: boolean;
}
@@ -8,6 +11,12 @@ const initialState: SearchSidebarState = {
sidebarCollapsed: true
};
/**
* Performs a search sidebar action on the current state
* @param {SearchSidebarState} state The state before the action is performed
* @param {SearchSidebarAction} action The action that should be performed
* @returns {SearchSidebarState} The state after the action is performed
*/
export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
switch (action.type) {

View File

@@ -9,27 +9,47 @@ import { HostWindowService } from '../../shared/host-window.service';
const sidebarStateSelector = (state: AppState) => state.searchSidebar;
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed);
/**
* Service that performs all actions that have to do with the search sidebar
*/
@Injectable()
export class SearchSidebarService {
/**
* Emits true is the current screen size is mobile
*/
private isXsOrSm$: Observable<boolean>;
private isCollapsdeInStored: Observable<boolean>;
/**
* Emits true is the sidebar's state in the store is currently collapsed
*/
private isCollapsedInStore: Observable<boolean>;
constructor(private store: Store<AppState>, private windowService: HostWindowService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.isCollapsdeInStored = this.store.select(sidebarCollapsedSelector);
this.isCollapsedInStore = this.store.select(sidebarCollapsedSelector);
}
/**
* Checks if the sidebar should currently be collapsed
* @returns {Observable<boolean>} Emits true if our screen size is mobile or when the state in the store is currently collapsed
*/
get isCollapsed(): Observable<boolean> {
return Observable.combineLatest(
this.isXsOrSm$,
this.isCollapsdeInStored,
this.isCollapsedInStore,
(mobile, store) => mobile ? store : true);
}
/**
* Dispatches a collapse action to the store
*/
public collapse(): void {
this.store.dispatch(new SearchSidebarCollapseAction());
}
/**
* Dispatches a expand action to the store
*/
public expand(): void {
this.store.dispatch(new SearchSidebarExpandAction());
}

View File

@@ -1,8 +1,6 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { RequestError } from '../data/request.models';
import { BrowseEntry } from '../shared/browse-entry.model';
import { PageInfo } from '../shared/page-info.model';
import { BrowseDefinition } from '../shared/browse-definition.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';
@@ -10,8 +8,6 @@ import { RegistryMetadataschemasResponse } from '../registry/registry-metadatasc
import { MetadataSchema } from '../metadata/metadataschema.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
import { AuthStatus } from '../auth/models/auth-status.model';
/* tslint:disable:max-classes-per-file */