second part of docs

This commit is contained in:
lotte
2018-07-26 14:25:30 +02:00
parent e7cc6a07f2
commit 38d1f6c85a
11 changed files with 259 additions and 55 deletions

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { FilterType } from '../../../search-service/filter-type.model';
import { renderFacetFor } from '../search-filter-type-decorator';
import {
@@ -7,11 +6,6 @@ import {
SearchFacetFilterComponent
} from '../search-facet-filter/search-facet-filter.component';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-boolean-filter',
@@ -20,6 +14,9 @@ import {
animations: [facetLoad]
})
/**
* Component that represents a boolean facet for a specific filter configuration
*/
@renderFacetFor(FilterType.boolean)
export class SearchBooleanFilterComponent extends SearchFacetFilterComponent implements OnInit {
}

View File

@@ -3,20 +3,32 @@ import { renderFilterType } from '../search-filter-type-decorator';
import { FilterType } from '../../../search-service/filter-type.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { FILTER_CONFIG } from '../search-filter.service';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'ds-search-facet-filter-wrapper',
templateUrl: './search-facet-filter-wrapper.component.html'
})
/**
* Wrapper component that renders a specific facet filter based on the filter config's type
*/
export class SearchFacetFilterWrapperComponent implements OnInit {
/**
* Configuration for the filter of this wrapper component
*/
@Input() filterConfig: SearchFilterConfig;
@Input() selectedValues: Observable<string[]>;
/**
* Injector to inject a child component with the @Input parameters
*/
objectInjector: Injector;
constructor(private injector: Injector) {
}
/**
* Initialize and add the filter config to the injector
*/
ngOnInit(): void {
this.objectInjector = Injector.create({
providers: [
@@ -26,7 +38,10 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
});
}
getSearchFilter(): string {
/**
* Find the correct component based on the filter config's type
*/
getSearchFilter() {
const type: FilterType = this.filterConfig.type;
return renderFilterType(type);
}

View File

@@ -16,27 +16,54 @@ import { SearchFilterConfig } from '../../../search-service/search-filter-config
import { SearchService } from '../../../search-service/search.service';
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-facet-filter',
template: ``,
})
/**
* Super class for all different representations of facets
*/
export class SearchFacetFilterComponent implements OnInit, OnDestroy {
filterValues: Array<Observable<RemoteData<PaginatedList<FacetValue>>>> = [];
/**
* Emits an array of pages with values found for this facet
*/
filterValues$: Subject<RemoteData<Array<PaginatedList<FacetValue>>>>;
/**
* Emits the current last shown page of this facet's values
*/
currentPage: Observable<number>;
/**
* Emits true if the current page is also the last page available
*/
isLastPage$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/**
* The value of the input field that is used to query for possible values for this filter
*/
filter: string;
/**
* List of subscriptions to unsubscribe from
*/
private subs: Subscription[] = [];
/**
* Emits the result values for this filter found by the current filter query
*/
filterSearchResults: Observable<any[]> = Observable.of([]);
/**
* Emits the active values for this filter
*/
selectedValues: Observable<string[]>;
private collapseNextUpdate = true;
/**
* State of the requested facets used to time the animation
*/
animationState = 'loading';
constructor(protected searchService: SearchService,
@@ -46,12 +73,15 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
}
/**
* Initializes all observable instance variables and starts listening to them
*/
ngOnInit(): void {
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
this.currentPage = this.getCurrentPage().distinctUntilChanged();
this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig);
const searchOptions = this.filterService.getSearchOptions().distinctUntilChanged();
this.subs.push(searchOptions.subscribe((options) => this.updateFilterValueList(options)));
this.subs.push(searchOptions.subscribe((options) => this.updateFilterValueList()));
const facetValues = Observable.combineLatest(searchOptions, this.currentPage, (options, page) => {
return {
@@ -59,7 +89,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
page: page
};
});
let filterValues = [];
this.subs.push(facetValues.subscribe((facetOutcome) => {
const newValues$ = facetOutcome.values;
@@ -69,12 +99,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.collapseNextUpdate = false;
}
if (facetOutcome.page === 1) {
this.filterValues = [];
filterValues = [];
}
this.filterValues = [...this.filterValues, newValues$];
filterValues = [...filterValues, newValues$];
this.subs.push(this.rdbs.aggregate(this.filterValues).subscribe((rd: RemoteData<Array<PaginatedList<FacetValue>>>) => {
this.subs.push(this.rdbs.aggregate(filterValues).subscribe((rd: RemoteData<Array<PaginatedList<FacetValue>>>) => {
this.animationState = 'ready';
this.filterValues$.next(rd);
}));
@@ -85,37 +115,61 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
}
updateFilterValueList(options: SearchOptions) {
/**
* Prepare for refreshing the values of this filter
*/
updateFilterValueList() {
this.animationState = 'loading';
this.collapseNextUpdate = true;
this.filter = '';
}
/**
* Checks if a value for this filter is currently active
*/
isChecked(value: FacetValue): Observable<boolean> {
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, value.value);
}
/**
* @returns {string} The base path to the search page
*/
getSearchLink() {
return this.searchService.getSearchLink();
}
/**
* Show the next page as well
*/
showMore() {
this.filterService.incrementPage(this.filterConfig.name);
}
/**
* Make sure only the first page is shown
*/
showFirstPageOnly() {
// this.filterValues = [];
this.filterService.resetPage(this.filterConfig.name);
}
/**
* @returns {Observable<number>} The current page of this filter
*/
getCurrentPage(): Observable<number> {
return this.filterService.getPage(this.filterConfig.name);
}
/**
* @returns {string} the current URL
*/
getCurrentUrl() {
return this.router.url;
}
/**
* Submits a new active custom value to the filter from the input field
* @param data The string from the input field
*/
onSubmit(data: any) {
this.selectedValues.first().subscribe((selectedValues) => {
if (isNotEmpty(data)) {
@@ -139,6 +193,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
return hasValue(o);
}
/**
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
* @param {string} value The value that is removed for this filter
* @returns {Observable<any>} The changed filter parameters
*/
getRemoveParams(value: string): Observable<any> {
return this.selectedValues.map((selectedValues) => {
return {
@@ -148,6 +207,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
});
}
/**
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
* @param {string} value The value that is added for this filter
* @returns {Observable<any>} The changed filter parameters
*/
getAddParams(value: string): Observable<any> {
return this.selectedValues.map((selectedValues) => {
return {
@@ -157,12 +221,20 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
});
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**
* Updates the found facet value suggestions for a given query
* Transforms the found values into display values
* @param data The query for which is being searched
*/
findSuggestions(data): void {
if (isNotEmpty(data)) {
this.filterService.getSearchOptions().first().subscribe(
@@ -183,6 +255,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
}
}
/**
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
* @param {FacetValue} facet The value of the facet as returned by the server
* @param {string} query The query that was used to search facet values
* @returns {string} The facet value with the query part emphasized
*/
getDisplayValue(facet: FacetValue, query: string): string {
return new EmphasizePipe().transform(facet.value, query) + ' (' + facet.count + ')';
}

View File

@@ -3,6 +3,11 @@ import { FilterType } from '../../search-service/filter-type.model';
const filterTypeMap = new Map();
/**
* Sets the mapping for a 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
*/
export function renderFacetFor(type: FilterType) {
return function decorator(objectElement: any) {
if (!objectElement) {
@@ -12,6 +17,11 @@ 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
*/
export function renderFilterType(type: FilterType) {
return filterTypeMap.get(type);
}

View File

@@ -5,12 +5,6 @@ import { Observable } from 'rxjs/Observable';
import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-filter',
styleUrls: ['./search-filter.component.scss'],

View File

@@ -1,16 +1,16 @@
import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
import { isEmpty } from '../../../shared/empty.util';
/*
Interface that represents the state for a single filters
/**
* Interface that represents the state for a single filters
*/
export interface SearchFilterState {
filterCollapsed: boolean,
page: number
}
/*
Interface that represents the state for all available filters
/**
* Interface that represents the state for all available filters
*/
export interface SearchFiltersState {
[name: string]: SearchFilterState

View File

@@ -34,22 +34,42 @@ export class SearchFilterService {
private route: ActivatedRoute) {
}
/**
* Checks if a given filter is active with a given value
* @param {string} paramName The parameter name of the filter's configuration for which to search
* @param {string} filterValue The value for which to search
* @returns {Observable<boolean>} Emit true when the filter is active with the given value
*/
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
return this.routeService.hasQueryParamWithValue(paramName, filterValue);
}
/**
* Checks if a given filter is active with any value
* @param {string} paramName The parameter name of the filter's configuration for which to search
* @returns {Observable<boolean>} Emit true when the filter is active with any value
*/
isFilterActive(paramName: string): Observable<boolean> {
return this.routeService.hasQueryParam(paramName);
}
/**
* @returns {Observable<string>} Emits the current scope's identifier
*/
getCurrentScope() {
return this.routeService.getQueryParameterValue('scope');
}
/**
* @returns {Observable<string>} Emits the current query string
*/
getCurrentQuery() {
return this.routeService.getQueryParameterValue('query');
}
/**
* @returns {Observable<string>} Emits the current pagination settings
*/
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
const page$ = this.routeService.getQueryParameterValue('page');
const size$ = this.routeService.getQueryParameterValue('pageSize');
@@ -61,6 +81,9 @@ export class SearchFilterService {
});
}
/**
* @returns {Observable<string>} Emits the current sorting settings
*/
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
const sortField$ = this.routeService.getQueryParameterValue('sortField');
@@ -75,6 +98,9 @@ export class SearchFilterService {
);
}
/**
* @returns {Observable<Params>} Emits the current active filters with their values as they are sent to the backend
*/
getCurrentFilters(): Observable<Params> {
return this.routeService.getQueryParamsWithPrefix('f.').map((filterParams) => {
if (isNotEmpty(filterParams)) {
@@ -97,14 +123,24 @@ export class SearchFilterService {
});
}
/**
* @returns {Observable<Params>} Emits the current active filters with their values as they are displayed in the frontend URL
*/
getCurrentFrontendFilters(): Observable<Params> {
return this.routeService.getQueryParamsWithPrefix('f.');
}
/**
* @returns {Observable<string>} Emits the current UI list view
*/
getCurrentView() {
return this.routeService.getQueryParameterValue('view');
}
/**
* @param defaults The default values for the search options, that will be used if nothing is explicitly set
* @returns {Observable<PaginatedSearchOptions>} Emits the current paginated search options
*/
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
return Observable.combineLatest(
this.getCurrentPagination(defaults.pagination),
@@ -129,6 +165,10 @@ export class SearchFilterService {
)
}
/**
* @param defaults The default values for the search options, that will be used if nothing is explicitly set
* @returns {Observable<PaginatedSearchOptions>} Emits the current search options
*/
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
return Observable.combineLatest(
this.getCurrentView(),
@@ -148,9 +188,14 @@ export class SearchFilterService {
)
}
/**
* Requests the active filter values set for a given filter
* @param {SearchFilterConfig} filterConfig The configuration for which the filters are active
* @returns {Observable<string[]>} Emits the active filters for the given filter configuration
*/
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').map((params: Params) => [].concat(...Object.values(params)));
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').map((params: Params) => [].concat(...Object.values(params)));
return Observable.combineLatest(values$, prefixValues$, (values, prefixValues) => {
if (isNotEmpty(values)) {
return values;
@@ -159,6 +204,11 @@ export class SearchFilterService {
})
}
/**
* Checks if the state of a given filter is currently collapsed or not
* @param {string} filterName The filtername for which the collapsed state is checked
* @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
*/
isCollapsed(filterName: string): Observable<boolean> {
return this.store.select(filterByNameSelector(filterName))
.map((object: SearchFilterState) => {
@@ -170,6 +220,11 @@ export class SearchFilterService {
});
}
/**
* Request the current page of a given filter
* @param {string} filterName The filtername for which the page state is checked
* @returns {Observable<boolean>} Emits the current page state of the given filter, if it's unavailable, return 1
*/
getPage(filterName: string): Observable<number> {
return this.store.select(filterByNameSelector(filterName))
.map((object: SearchFilterState) => {
@@ -181,34 +236,65 @@ export class SearchFilterService {
});
}
/**
* Dispatches a collapse action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public collapse(filterName: string): void {
this.store.dispatch(new SearchFilterCollapseAction(filterName));
}
/**
* Dispatches a expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public expand(filterName: string): void {
this.store.dispatch(new SearchFilterExpandAction(filterName));
}
/**
* Dispatches a toggle action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public toggle(filterName: string): void {
this.store.dispatch(new SearchFilterToggleAction(filterName));
}
/**
* Dispatches a initial collapse action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public initialCollapse(filterName: string): void {
this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
}
/**
* Dispatches a initial expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public initialExpand(filterName: string): void {
this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
}
/**
* Dispatches a decrement action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public decrementPage(filterName: string): void {
this.store.dispatch(new SearchFilterDecrementPageAction(filterName));
}
/**
* Dispatches a increment page action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public incrementPage(filterName: string): void {
this.store.dispatch(new SearchFilterIncrementPageAction(filterName));
}
/**
* Dispatches a reset page action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public resetPage(filterName: string): void {
this.store.dispatch(new SearchFilterResetPageAction(filterName));
}

View File

@@ -1,6 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { FacetValue } from '../../../search-service/facet-value.model';
import { Observable } from 'rxjs/Observable';
import { FilterType } from '../../../search-service/filter-type.model';
import { renderFacetFor } from '../search-filter-type-decorator';
import {
@@ -8,12 +6,6 @@ import {
SearchFacetFilterComponent
} from '../search-facet-filter/search-facet-filter.component';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-hierarchy-filter',
styleUrls: ['./search-hierarchy-filter.component.scss'],
@@ -21,6 +13,9 @@ import {
animations: [facetLoad]
})
/**
* Component that represents a hierarchy facet for a specific filter configuration
*/
@renderFacetFor(FilterType.hierarchy)
export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit {
}

View File

@@ -28,7 +28,7 @@
<ng-container *ngFor="let value of page.page; let i=index">
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge">
[queryParams]="getChangeParams(value.value) | async" queryParamsHandling="merge">
<span class="filter-value px-1">{{value.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>

View File

@@ -34,11 +34,29 @@ const rangeDelimiter = '-';
animations: [facetLoad]
})
/**
* Component that represents a range facet for a specific filter configuration
*/
@renderFacetFor(FilterType.range)
export class SearchRangeFilterComponent extends SearchFacetFilterComponent implements OnInit, OnDestroy {
/**
* Fallback minimum for the range
*/
min = 1950;
/**
* Fallback maximum for the range
*/
max = 2018;
/**
* The current range of the filter
*/
range;
/**
* Subscription to unsubscribe from
*/
sub: Subscription;
constructor(protected searchService: SearchService,
@@ -52,6 +70,10 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
}
/**
* Initialize with the min and max values as configured in the filter configuration
* Set the initial values of the range
*/
ngOnInit(): void {
super.ngOnInit();
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
@@ -65,7 +87,12 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
}).subscribe((minmax) => this.range = minmax);
}
getAddParams(value: string) {
/**
* Calculates the parameters that should change if a given values for this range filter would be changed
* @param {string} value The values that are changed for this filter
* @returns {Observable<any>} The changed filter parameters
*/
getChangeParams(value: string) {
const parts = value.split(rangeDelimiter);
const min = parts.length > 1 ? parts[0].trim() : value;
const max = parts.length > 1 ? parts[1].trim() : value;
@@ -77,16 +104,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
});
}
getRemoveParams(value: string) {
return Observable.of(
{
[this.filterConfig.paramName + minSuffix]: null,
[this.filterConfig.paramName + maxSuffix]: null,
page: 1
}
);
}
/**
* Submits new custom range values to the range filter from the widget
*/
onSubmit() {
const newMin = this.range[0] !== this.min ? [this.range[0]] : null;
const newMax = this.range[1] !== this.max ? [this.range[1]] : null;
@@ -103,12 +123,18 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
/**
* TODO when upgrading nouislider, verify that this check is still needed.
* Prevents AoT bug
* @returns {boolean} True if the platformId is a platform browser
*/
shouldShowSlider(): boolean {
return isPlatformBrowser(this.platformId);
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy() {
super.ngOnDestroy();
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}

View File

@@ -21,6 +21,9 @@ import { renderFacetFor } from '../search-filter-type-decorator';
animations: [facetLoad]
})
/**
* Component that represents a text facet for a specific filter configuration
*/
@renderFacetFor(FilterType.text)
export class SearchTextFilterComponent extends SearchFacetFilterComponent implements OnInit {
}