Fixed issue #1 and some other fixes

This commit is contained in:
lotte
2018-07-30 10:19:45 +02:00
parent 1ab53e7204
commit 3fa70523ea
55 changed files with 450 additions and 135 deletions

View File

@@ -2,7 +2,6 @@ import 'rxjs/add/observable/of';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from './paginated-search-options.model'; import { PaginatedSearchOptions } from './paginated-search-options.model';
import { ViewMode } from './search-options.model';
describe('PaginatedSearchOptions', () => { describe('PaginatedSearchOptions', () => {
let options: PaginatedSearchOptions; let options: PaginatedSearchOptions;
@@ -19,7 +18,6 @@ describe('PaginatedSearchOptions', () => {
options.filters = filters; options.filters = filters;
options.query = query; options.query = query;
options.scope = scope; options.scope = scope;
options.view = ViewMode.Grid;
}); });
describe('when toRestUrl is called', () => { describe('when toRestUrl is called', () => {

View File

@@ -91,8 +91,6 @@ 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 = [mockValues];
// comp.filterValues$ = new BehaviorSubject({});
filterService = (comp as any).filterService; filterService = (comp as any).filterService;
searchService = (comp as any).searchService; searchService = (comp as any).searchService;
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues); spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
@@ -198,10 +196,9 @@ describe('SearchFacetFilterComponent', () => {
}); });
describe('when updateFilterValueList is called', () => { describe('when updateFilterValueList is called', () => {
const searchOptions = new SearchOptions();
beforeEach(() => { beforeEach(() => {
spyOn(comp, 'showFirstPageOnly'); spyOn(comp, 'showFirstPageOnly');
comp.updateFilterValueList(searchOptions) comp.updateFilterValueList()
}); });
it('should call showFirstPageOnly and empty the filter', () => { it('should call showFirstPageOnly and empty the filter', () => {
@@ -211,7 +208,6 @@ describe('SearchFacetFilterComponent', () => {
}); });
}); });
describe('when findSuggestions is called with query \'test\'', () => { describe('when findSuggestions is called with query \'test\'', () => {
const query = 'test'; const query = 'test';
beforeEach(() => { beforeEach(() => {

View File

@@ -189,6 +189,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.filter = data; this.filter = data;
} }
/**
* For usage of the hasValue function in the template
*/
hasValue(o: any): boolean { hasValue(o: any): boolean {
return hasValue(o); return hasValue(o);
} }

View File

@@ -248,7 +248,7 @@ export class SearchFilterService {
} }
/** /**
* Dispatches a expand action to the store for a given filter * Dispatches an expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched * @param {string} filterName The filter for which the action is dispatched
*/ */
public expand(filterName: string): void { public expand(filterName: string): void {
@@ -264,7 +264,7 @@ export class SearchFilterService {
} }
/** /**
* Dispatches a initial collapse action to the store for a given filter * Dispatches an initial collapse action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched * @param {string} filterName The filter for which the action is dispatched
*/ */
public initialCollapse(filterName: string): void { public initialCollapse(filterName: string): void {
@@ -272,7 +272,7 @@ export class SearchFilterService {
} }
/** /**
* Dispatches a initial expand action to the store for a given filter * Dispatches an initial expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched * @param {string} filterName The filter for which the action is dispatched
*/ */
public initialExpand(filterName: string): void { public initialExpand(filterName: string): void {
@@ -288,7 +288,7 @@ export class SearchFilterService {
} }
/** /**
* Dispatches a increment page action to the store for a given filter * Dispatches an increment page action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched * @param {string} filterName The filter for which the action is dispatched
*/ */
public incrementPage(filterName: string): void { public incrementPage(filterName: string): void {

View File

@@ -96,7 +96,6 @@ describe('SearchRangeFilterComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SearchRangeFilterComponent); fixture = TestBed.createComponent(SearchRangeFilterComponent);
comp = fixture.componentInstance; // SearchPageComponent test instance comp = fixture.componentInstance; // SearchPageComponent test instance
comp.filterValues = [mockValues];
filterService = (comp as any).filterService; filterService = (comp as any).filterService;
searchService = (comp as any).searchService; searchService = (comp as any).searchService;
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues); spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
@@ -104,9 +103,9 @@ describe('SearchRangeFilterComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('when the getAddParams method is called wih a value', () => { describe('when the getChangeParams method is called wih a value', () => {
it('should return the selectedValue list with the new parameter value', () => { it('should return the selectedValue list with the new parameter value', () => {
const result$ = comp.getAddParams(value3); const result$ = comp.getChangeParams(value3);
result$.subscribe((result) => { result$.subscribe((result) => {
expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']); expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']);
expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']); expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']);
@@ -114,16 +113,6 @@ describe('SearchRangeFilterComponent', () => {
}); });
}); });
describe('when the getRemoveParams method is called wih a value', () => {
it('should return the selectedValue list with the parameter value left out', () => {
const result$ = comp.getRemoveParams(value1);
result$.subscribe((result) => {
expect(result[mockFilterConfig.paramName + minSuffix]).toBeNull();
expect(result[mockFilterConfig.paramName + maxSuffix]).toBeNull();
});
});
});
describe('when the onSubmit method is called with data', () => { describe('when the onSubmit method is called with data', () => {
const searchUrl = '/search/path'; const searchUrl = '/search/path';

View File

@@ -1,6 +1,6 @@
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { PaginatedSearchOptions } from './paginated-search-options.model'; import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchOptions, ViewMode } from './search-options.model'; import { SearchOptions } from './search-options.model';
describe('SearchOptions', () => { describe('SearchOptions', () => {
let options: PaginatedSearchOptions; let options: PaginatedSearchOptions;
@@ -13,7 +13,6 @@ describe('SearchOptions', () => {
options.filters = filters; options.filters = filters;
options.query = query; options.query = query;
options.scope = scope; options.scope = scope;
options.view = ViewMode.Grid;
}); });
describe('when toRestUrl is called', () => { describe('when toRestUrl is called', () => {

View File

@@ -2,20 +2,10 @@ import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
import 'core-js/library/fn/object/entries'; import 'core-js/library/fn/object/entries';
/**
* This enumeration represents all possible ways of representing the a group of objects
*/
export enum ViewMode {
List = 'list',
Grid = 'grid'
}
/** /**
* This model class represents all parameters needed to request information about a certain search request * This model class represents all parameters needed to request information about a certain search request
*/ */
export class SearchOptions { export class SearchOptions {
view?: ViewMode = ViewMode.List;
scope?: string; scope?: string;
query?: string; query?: string;
filters?: any; filters?: any;

View File

@@ -31,7 +31,7 @@
</button> </button>
</div> </div>
<ds-search-results [searchResults]="resultsRD$ | async" <ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async" [sortConfig]="sortConfig"></ds-search-results> [searchConfig]="searchOptions$ | async"></ds-search-results>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,9 +12,9 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte
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';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -38,13 +38,12 @@ export class SearchPageComponent implements OnInit {
/** /**
* The current search results * The current search results
*/ */
resultsRD$: Subject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new Subject(); resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
/** /**
* The current paginated search options * The current paginated search options
*/ */
searchOptions$: Observable<PaginatedSearchOptions>; searchOptions$: Observable<PaginatedSearchOptions>;
sortConfig: SortOptions;
/** /**
* The current relevant scopes * The current relevant scopes
@@ -91,8 +90,7 @@ export class SearchPageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults); this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults);
this.sub = this.searchOptions$.subscribe((searchOptions) => this.sub = this.searchOptions$.subscribe((searchOptions) =>
this.service.search(searchOptions).first().subscribe((results) => this.resultsRD$.next(results)) this.service.search(searchOptions).filter((rd) => !rd.isLoading).first().subscribe((results) => this.resultsRD$.next(results)));
);
this.scopeListRD$ = this.filterService.getCurrentScope().pipe( this.scopeListRD$ = this.filterService.getCurrentScope().pipe(
flatMap((scopeId) => this.service.getScopes(scopeId)) flatMap((scopeId) => this.service.getScopes(scopeId))
@@ -107,7 +105,7 @@ export class SearchPageComponent implements OnInit {
} }
/** /**
* Set the sidebar to a expanded state * Set the sidebar to an expanded state
*/ */
public openSidebar(): void { public openSidebar(): void {
this.sidebarService.expand(); this.sidebarService.expand();

View File

@@ -82,5 +82,9 @@ const effects = [
SearchBooleanFilterComponent, SearchBooleanFilterComponent,
] ]
}) })
/**
* This module handles all components and pipes that are necessary for the search page
*/
export class SearchPageModule { export class SearchPageModule {
} }

View File

@@ -2,10 +2,10 @@ import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { SearchOptions, ViewMode } from '../search-options.model'; import { SearchOptions } from '../search-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'; import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-search-results', selector: 'ds-search-results',
@@ -30,13 +30,9 @@ export class SearchResultsComponent {
*/ */
@Input() searchConfig: SearchOptions; @Input() searchConfig: SearchOptions;
/**
* The current sort options for the search
*/
@Input() sortConfig: SortOptions;
/** /**
* The current view mode for the search results * The current view mode for the search results
*/ */
@Input() viewMode: ViewMode; @Input() viewMode: ViewMode;
} }

View File

@@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { ViewMode } from '../../+search-page/search-options.model';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
@@ -28,6 +27,7 @@ import { SearchQueryResponse } from './search-query-response.model';
import { SearchFilterConfig } from './search-filter-config.model'; import { SearchFilterConfig } from './search-filter-config.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {

View File

@@ -1,13 +1,14 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { import {
ActivatedRoute, NavigationExtras, Params, PRIMARY_OUTLET, Router, ActivatedRoute,
NavigationExtras,
PRIMARY_OUTLET,
Router,
UrlSegmentGroup UrlSegmentGroup
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { filter, flatMap, map, tap } from 'rxjs/operators'; import { flatMap, map } from 'rxjs/operators';
import { ViewMode } from '../../+search-page/search-options.model';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { import {
FacetConfigSuccessResponse, FacetConfigSuccessResponse,
FacetValueSuccessResponse, FacetValueSuccessResponse,
@@ -25,8 +26,7 @@ import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { configureRequest } from '../../core/shared/operators'; import { configureRequest } from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { NormalizedSearchResult } from '../normalized-search-result.model'; 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';
@@ -44,6 +44,7 @@ import { Community } from '../../core/shared/community.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { ViewMode } from '../../core/shared/view-mode.model';
/** /**

View File

@@ -13,6 +13,7 @@ import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
import { hot } from 'jasmine-marbles'; import { hot } from 'jasmine-marbles';
import { VarDirective } from '../../shared/utils/var.directive';
describe('SearchSettingsComponent', () => { describe('SearchSettingsComponent', () => {
@@ -56,7 +57,7 @@ describe('SearchSettingsComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
declarations: [SearchSettingsComponent, EnumKeysPipe], declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective],
providers: [ providers: [
{ provide: SearchService, useValue: searchServiceStub }, { provide: SearchService, useValue: searchServiceStub },
@@ -96,15 +97,18 @@ describe('SearchSettingsComponent', () => {
}); });
it('it should show the order settings with the respective selectable options', () => { it('it should show the order settings with the respective selectable options', () => {
(comp as any).searchOptions$.first().subscribe((options) => {
fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
expect(orderSetting).toBeDefined(); expect(orderSetting).toBeDefined();
const childElements = orderSetting.query(By.css('.form-control')).children; const childElements = orderSetting.query(By.css('.form-control')).children;
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length); expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
}); });
});
it('it should show the size settings with the respective selectable options', () => { it('it should show the size settings with the respective selectable options', () => {
(comp as any).filterService.getPaginatedSearchOptions().first().subscribe((options) => { (comp as any).searchOptions$.first().subscribe((options) => {
fixture.detectChanges() fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
expect(pageSizeSetting).toBeDefined(); expect(pageSizeSetting).toBeDefined();
const childElements = pageSizeSetting.query(By.css('.form-control')).children; const childElements = pageSizeSetting.query(By.css('.form-control')).children;
@@ -114,15 +118,21 @@ describe('SearchSettingsComponent', () => {
}); });
it('should have the proper order value selected by default', () => { it('should have the proper order value selected by default', () => {
(comp as any).searchOptions$.first().subscribe((options) => {
fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]')) const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'));
expect(childElementToBeSelected).toBeDefined(); expect(childElementToBeSelected).toBeDefined();
}); });
});
it('should have the proper rpp value selected by default', () => { it('should have the proper rpp value selected by default', () => {
(comp as any).searchOptions$.first().subscribe((options) => {
fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]')) const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'));
expect(childElementToBeSelected).toBeDefined(); expect(childElementToBeSelected).toBeDefined();
}); });
});
}); });

View File

@@ -18,7 +18,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
export class SearchSidebarComponent { export class SearchSidebarComponent {
/** /**
* The total amount of result * The total amount of results
*/ */
@Input() resultCount; @Input() resultCount;

View File

@@ -31,7 +31,7 @@ export class SearchSidebarService {
/** /**
* Checks if the sidebar should currently be collapsed * 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 * @returns {Observable<boolean>} Emits true if the user's screen size is mobile or when the state in the store is currently collapsed
*/ */
get isCollapsed(): Observable<boolean> { get isCollapsed(): Observable<boolean> {
return Observable.combineLatest( return Observable.combineLatest(
@@ -48,7 +48,7 @@ export class SearchSidebarService {
} }
/** /**
* Dispatches a expand action to the store * Dispatches an expand action to the store
*/ */
public expand(): void { public expand(): void {
this.store.dispatch(new SearchSidebarExpandAction()); this.store.dispatch(new SearchSidebarExpandAction());

View File

@@ -0,0 +1,8 @@
/**
* This enumeration represents all possible ways of representing a group of objects in the UI
*/
export enum ViewMode {
List = 'list',
Grid = 'grid'
}

View File

@@ -9,13 +9,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { hasValue, isNotEmpty } from '../empty.util'; import { hasValue, isNotEmpty } from '../empty.util';
import { ActivatedRoute } from '@angular/router';
/**
* 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({ @Component({
selector: 'ds-input-suggestions', selector: 'ds-input-suggestions',
@@ -23,28 +16,92 @@ import { ActivatedRoute } from '@angular/router';
templateUrl: './input-suggestions.component.html' templateUrl: './input-suggestions.component.html'
}) })
/**
* Component representing a form with a autocomplete functionality
*/
export class InputSuggestionsComponent { export class InputSuggestionsComponent {
/**
* The suggestions that should be shown
*/
@Input() suggestions: any[] = []; @Input() suggestions: any[] = [];
/**
* The time waited to detect if any other input will follow before requesting the suggestions
*/
@Input() debounceTime = 500; @Input() debounceTime = 500;
/**
* Placeholder attribute for the input field
*/
@Input() placeholder = ''; @Input() placeholder = '';
/**
* Action attribute for the form
*/
@Input() action; @Input() action;
/**
* Name attribute for the input field
*/
@Input() name; @Input() name;
/**
* Value of the input field
*/
@Input() ngModel; @Input() ngModel;
/**
* Output for when the input field's value changes
*/
@Output() ngModelChange = new EventEmitter(); @Output() ngModelChange = new EventEmitter();
/**
* Output for when the form is submitted
*/
@Output() submitSuggestion = new EventEmitter(); @Output() submitSuggestion = new EventEmitter();
/**
* Output for when a suggestion is clicked
*/
@Output() clickSuggestion = new EventEmitter(); @Output() clickSuggestion = new EventEmitter();
/**
* Output for when new suggestions should be requested
*/
@Output() findSuggestions = new EventEmitter(); @Output() findSuggestions = new EventEmitter();
/**
* Emits true when the list of suggestions should be shown
*/
show = new BehaviorSubject<boolean>(false); show = new BehaviorSubject<boolean>(false);
/**
* Index of the currently selected suggestion
*/
selectedIndex = -1; selectedIndex = -1;
/**
* Reference to the input field component
*/
@ViewChild('inputField') queryInput: ElementRef; @ViewChild('inputField') queryInput: ElementRef;
/**
* Reference to the suggestion components
*/
@ViewChildren('suggestion') resultViews: QueryList<ElementRef>; @ViewChildren('suggestion') resultViews: QueryList<ElementRef>;
/**
* When any of the inputs change, check if we should still show the suggestions
*/
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (hasValue(changes.suggestions)) { if (hasValue(changes.suggestions)) {
this.show.next(isNotEmpty(changes.suggestions.currentValue)); this.show.next(isNotEmpty(changes.suggestions.currentValue));
} }
} }
/**
* Move the focus on one of the suggestions up to the previous suggestion
* When no suggestion is currently in focus OR the first suggestion is in focus: shift to the last suggestion
*/
shiftFocusUp(event: KeyboardEvent) { shiftFocusUp(event: KeyboardEvent) {
event.preventDefault(); event.preventDefault();
if (this.selectedIndex > 0) { if (this.selectedIndex > 0) {
@@ -56,6 +113,10 @@ export class InputSuggestionsComponent {
this.changeFocus(); this.changeFocus();
} }
/**
* Move the focus on one of the suggestions up to the next suggestion
* When no suggestion is currently in focus OR the last suggestion is in focus: shift to the first suggestion
*/
shiftFocusDown(event: KeyboardEvent) { shiftFocusDown(event: KeyboardEvent) {
event.preventDefault(); event.preventDefault();
if (this.selectedIndex >= 0) { if (this.selectedIndex >= 0) {
@@ -67,26 +128,42 @@ export class InputSuggestionsComponent {
this.changeFocus(); this.changeFocus();
} }
/**
* Perform the change of focus to the current selectedIndex
*/
changeFocus() { changeFocus() {
if (this.resultViews.length > 0) { if (this.resultViews.length > 0) {
this.resultViews.toArray()[this.selectedIndex].nativeElement.focus(); this.resultViews.toArray()[this.selectedIndex].nativeElement.focus();
} }
} }
/**
* When any key is pressed (except for the Enter button) the query input should move to the input field
* @param {KeyboardEvent} event The keyboard event
*/
onKeydown(event: KeyboardEvent) { onKeydown(event: KeyboardEvent) {
if (event.key !== 'Enter') { if (event.key !== 'Enter') {
this.queryInput.nativeElement.focus(); this.queryInput.nativeElement.focus();
} }
} }
/**
* Changes the show variable so the suggestion dropdown closes
*/
close() { close() {
this.show.next(false); this.show.next(false);
} }
/**
* For usage of the isNotEmpty function in the template
*/
isNotEmpty(data) { isNotEmpty(data) {
return isNotEmpty(data); return isNotEmpty(data);
} }
/**
* Make sure that if a suggestion is clicked, the suggestions dropdown closes and the focus moves to the input field
*/
onClickSuggestion(data) { onClickSuggestion(data) {
this.clickSuggestion.emit(data); this.clickSuggestion.emit(data);
this.close(); this.close();

View File

@@ -1,13 +1,11 @@
import { ObjectCollectionComponent } from './object-collection.component'; import { ObjectCollectionComponent } from './object-collection.component';
import { ViewMode } from '../../+search-page/search-options.model';
import { element } from 'protractor';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Config } from '../../../config/config.interface'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RouterStub } from '../testing/router-stub'; import { RouterStub } from '../testing/router-stub';
import { ViewMode } from '../../core/shared/view-mode.model';
describe('ObjectCollectionComponent', () => { describe('ObjectCollectionComponent', () => {
let fixture: ComponentFixture<ObjectCollectionComponent>; let fixture: ComponentFixture<ObjectCollectionComponent>;

View File

@@ -14,8 +14,8 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ListableObject } from './shared/listable-object.model'; import { ListableObject } from './shared/listable-object.model';
import { ViewMode } from '../../+search-page/search-options.model';
import { hasValue, isNotEmpty } from '../empty.util'; import { hasValue, isNotEmpty } from '../empty.util';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-viewable-collection', selector: 'ds-viewable-collection',

View File

@@ -1,6 +1,6 @@
import { ViewMode } from '../../../+search-page/search-options.model';
import { renderElementsFor } from './dso-element-decorator'; import { renderElementsFor } from './dso-element-decorator';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
describe('ElementDecorator', () => { describe('ElementDecorator', () => {
const gridDecorator = renderElementsFor(Item, ViewMode.Grid); const gridDecorator = renderElementsFor(Item, ViewMode.Grid);

View File

@@ -1,6 +1,6 @@
import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../core/shared/generic-constructor';
import { ListableObject } from './listable-object.model'; import { ListableObject } from './listable-object.model';
import { ViewMode } from '../../../+search-page/search-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
const dsoElementMap = new Map(); const dsoElementMap = new Map();
export function renderElementsFor(listable: GenericConstructor<ListableObject>, viewMode: ViewMode) { export function renderElementsFor(listable: GenericConstructor<ListableObject>, viewMode: ViewMode) {

View File

@@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-collection-grid-element', selector: 'ds-collection-grid-element',

View File

@@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-community-grid-element', selector: 'ds-community-grid-element',

View File

@@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ViewMode } from '../../../+search-page/search-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-item-grid-element', selector: 'ds-item-grid-element',

View File

@@ -3,8 +3,8 @@ import { Component } from '@angular/core';
import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator'; import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-collection-search-result-grid-element', selector: 'ds-collection-search-result-grid-element',

View File

@@ -2,8 +2,8 @@ import { Component } from '@angular/core';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../../core/shared/community.model';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-community-search-result-grid-element', selector: 'ds-community-search-result-grid-element',

View File

@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { focusShadow } from '../../../../shared/animations/focus'; import { focusShadow } from '../../../../shared/animations/focus';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-item-search-result-grid-element', selector: 'ds-item-search-result-grid-element',

View File

@@ -1,8 +1,8 @@
import { Component, Injector, Input, OnInit } from '@angular/core'; import { Component, Injector, Input, OnInit } from '@angular/core';
import { ViewMode } from '../../../+search-page/search-options.model';
import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../core/shared/generic-constructor';
import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator'; import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator';
import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-wrapper-grid-element', selector: 'ds-wrapper-grid-element',

View File

@@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-collection-list-element', selector: 'ds-collection-list-element',

View File

@@ -1,9 +1,9 @@
import { Component, Input, Inject } from '@angular/core'; import { Component } from '@angular/core';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-community-list-element', selector: 'ds-community-list-element',

View File

@@ -1,9 +1,9 @@
import { Component, Input, Inject } from '@angular/core'; import { Component } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { ViewMode } from '../../../+search-page/search-options.model'; import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-item-list-element', selector: 'ds-item-list-element',

View File

@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-collection-search-result-list-element', selector: 'ds-collection-search-result-list-element',

View File

@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../../core/shared/community.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-community-search-result-list-element', selector: 'ds-community-search-result-list-element',

View File

@@ -1,12 +1,11 @@
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { ListableObject } from '../../../object-collection/shared/listable-object.model';
import { focusBackground } from '../../../animations/focus'; import { focusBackground } from '../../../animations/focus';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-item-search-result-list-element', selector: 'ds-item-search-result-list-element',

View File

@@ -1,8 +1,8 @@
import { Component, Injector, Input, OnInit } from '@angular/core'; import { Component, Injector, Input, OnInit } from '@angular/core';
import { ViewMode } from '../../../+search-page/search-options.model';
import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../core/shared/generic-constructor';
import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator' import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator'
import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-wrapper-list-element', selector: 'ds-wrapper-list-element',

View File

@@ -1,6 +1,6 @@
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search"> <form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search">
<div *ngIf="isNotEmpty(scopes)" class="col-12 col-sm-3"> <div *ngIf="isNotEmpty(scopes)" class="col-12 col-sm-3">
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" (change)="onScopeChange($event.target.value)"> <select [(ngModel)]="scope" name="scope" class="form-control" aria-label="Search scope" (change)="onScopeChange($event.target.value)">
<option value>{{'search.form.search_dspace' | translate}}</option> <option value>{{'search.form.search_dspace' | translate}}</option>
<option *ngFor="let scopeOption of scopes" [value]="scopeOption.id">{{scopeOption?.name ? scopeOption.name : 'search.form.search_dspace' | translate}}</option> <option *ngFor="let scopeOption of scopes" [value]="scopeOption.id">{{scopeOption?.name ? scopeOption.name : 'search.form.search_dspace' | translate}}</option>
</select> </select>

View File

@@ -3,6 +3,7 @@ import { SearchService } from '../../+search-page/search-service/search.service'
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util'; import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util';
import { QueryParamsHandling } from '@angular/router/src/config';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -15,36 +16,66 @@ import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util';
styleUrls: ['./search-form.component.scss'], styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html' templateUrl: './search-form.component.html'
}) })
export class SearchFormComponent {
@Input() query: string;
selectedId = '';
@Input() currentUrl: string;
@Input() scopes: DSpaceObject[];
/**
* Component that represents the search form
*/
export class SearchFormComponent {
/**
* The search query
*/
@Input() query: string;
/**
* The currently selected scope object's UUID
*/
@Input() @Input()
set scope(id: string) { scope = '';
this.selectedId = id; @Input() currentUrl: string;
}
/**
* The available scopes
*/
@Input() scopes: DSpaceObject[];
constructor(private router: Router) { constructor(private router: Router) {
} }
/**
* Updates the search when the form is submitted
* @param data Values submitted using the form
*/
onSubmit(data: any) { onSubmit(data: any) {
this.updateSearch(data); this.updateSearch(data);
} }
/**
* Updates the search when the current scope has been changed
* @param {string} scope The new scope
*/
onScopeChange(scope: string) { onScopeChange(scope: string) {
this.updateSearch({ scope }); this.updateSearch({ scope });
} }
/**
* Updates the search URL
* @param data Updated parameters
*/
updateSearch(data: any) { updateSearch(data: any) {
const newUrl = hasValue(this.currentUrl) ? this.currentUrl : 'search'; const newUrl = hasValue(this.currentUrl) ? this.currentUrl : '/search';
let handling: QueryParamsHandling = '' ;
if (this.currentUrl === '/search') {
handling = 'merge';
}
this.router.navigate([newUrl], { this.router.navigate([newUrl], {
queryParams: Object.assign({}, { page: 1 }, data), queryParams: Object.assign({}, { page: 1 }, data),
queryParamsHandling: 'merge' queryParamsHandling: handling
}); });
} }
/**
* For usage of the isNotEmpty function in the template
*/
isNotEmpty(object: any) { isNotEmpty(object: any) {
return isNotEmpty(object); return isNotEmpty(object);
} }

View File

@@ -156,6 +156,10 @@ const DIRECTIVES = [
...ENTRY_COMPONENTS ...ENTRY_COMPONENTS
] ]
}) })
/**
* This module handles all components and pipes that need to be shared among multiple other modules
*/
export class SharedModule { export class SharedModule {
} }

View File

@@ -1,6 +1,4 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { ViewMode } from '../../+search-page/search-options.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class HALEndpointServiceStub { export class HALEndpointServiceStub {

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { ViewMode } from '../../+search-page/search-options.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ViewMode } from '../../core/shared/view-mode.model';
export class SearchServiceStub { export class SearchServiceStub {

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { TruncatableService } from '../truncatable.service'; import { TruncatableService } from '../truncatable.service';
import { hasValue } from '../../empty.util';
@Component({ @Component({
selector: 'ds-truncatable-part', selector: 'ds-truncatable-part',
@@ -7,22 +8,59 @@ import { TruncatableService } from '../truncatable.service';
styleUrls: ['./truncatable-part.component.scss'] styleUrls: ['./truncatable-part.component.scss']
}) })
/**
* Component that truncates/clamps a piece of text
* It needs a TruncatableComponent parent to identify it's current state
*/
export class TruncatablePartComponent implements OnInit, OnDestroy { export class TruncatablePartComponent implements OnInit, OnDestroy {
/**
* Number of lines shown when the part is collapsed
*/
@Input() minLines: number; @Input() minLines: number;
/**
* Number of lines shown when the part is expanded. -1 indicates no limit
*/
@Input() maxLines = -1; @Input() maxLines = -1;
/**
* The identifier of the parent TruncatableComponent
*/
@Input() id: string; @Input() id: string;
/**
* Type of text, can be a h4 for headers or any other class you want to add
*/
@Input() type: string; @Input() type: string;
/**
* True if the minimal height of the part should at least be as high as it's minimum amount of lines
*/
@Input() fixedHeight = false; @Input() fixedHeight = false;
/**
* Current amount of lines shown of this part
*/
lines: string; lines: string;
/**
* Subscription to unsubscribe from
*/
private sub; private sub;
public constructor(private service: TruncatableService) { public constructor(private service: TruncatableService) {
} }
/**
* Initialize lines variable
*/
ngOnInit() { ngOnInit() {
this.setLines(); this.setLines();
} }
/**
* Subscribe to the current state to determine how much lines should be shown of this part
*/
private setLines() { private setLines() {
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => { this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
if (collapsed) { if (collapsed) {
@@ -33,7 +71,12 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
}); });
} }
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void { ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
}
} }

View File

@@ -16,22 +16,43 @@ export const TruncatableActionTypes = {
}; };
export class TruncatableAction implements Action { export class TruncatableAction implements Action {
/**
* UUID of the truncatable component the action is performed on, used to identify the filter
*/
id: string; id: string;
/**
* Type of action that will be performed
*/
type; type;
constructor(name: string) {
this.id = name; /**
* Initialize with the truncatable component's UUID
* @param {string} id of the filter
*/
constructor(id: string) {
this.id = id;
} }
} }
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
/**
* Used to collapse a truncatable component when it's expanded and expand it when it's collapsed
*/
export class TruncatableToggleAction extends TruncatableAction { export class TruncatableToggleAction extends TruncatableAction {
type = TruncatableActionTypes.TOGGLE; type = TruncatableActionTypes.TOGGLE;
} }
/**
* Used to collapse a truncatable component
*/
export class TruncatableCollapseAction extends TruncatableAction { export class TruncatableCollapseAction extends TruncatableAction {
type = TruncatableActionTypes.COLLAPSE; type = TruncatableActionTypes.COLLAPSE;
} }
/**
* Used to expand a truncatable component
*/
export class TruncatableExpandAction extends TruncatableAction { export class TruncatableExpandAction extends TruncatableAction {
type = TruncatableActionTypes.EXPAND; type = TruncatableActionTypes.EXPAND;
} }

View File

@@ -9,14 +9,32 @@ import { TruncatableService } from './truncatable.service';
styleUrls: ['./truncatable.component.scss'], styleUrls: ['./truncatable.component.scss'],
}) })
/**
* Component that represents a section with one or more truncatable parts that all listen to this state
*/
export class TruncatableComponent { export class TruncatableComponent {
/**
* Is true when all truncatable parts in this truncatable should be expanded on loading
*/
@Input() initialExpand = false; @Input() initialExpand = false;
/**
* The unique identifier of this truncatable component
*/
@Input() id: string; @Input() id: string;
/**
* Is true when the truncatable should expand on both hover as click
*/
@Input() onHover = false; @Input() onHover = false;
public constructor(private service: TruncatableService) { public constructor(private service: TruncatableService) {
} }
/**
* Set the initial state
*/
ngOnInit() { ngOnInit() {
if (this.initialExpand) { if (this.initialExpand) {
this.service.expand(this.id); this.service.expand(this.id);
@@ -25,18 +43,27 @@ export class TruncatableComponent {
} }
} }
/**
* If onHover is true, collapses the truncatable
*/
public hoverCollapse() { public hoverCollapse() {
if (this.onHover) { if (this.onHover) {
this.service.collapse(this.id); this.service.collapse(this.id);
} }
} }
/**
* If onHover is true, expands the truncatable
*/
public hoverExpand() { public hoverExpand() {
if (this.onHover) { if (this.onHover) {
this.service.expand(this.id); this.service.expand(this.id);
} }
} }
/**
* Expands the truncatable when it's collapsed, collapses it when it's expanded
*/
public toggle() { public toggle() {
this.service.toggle(this.id); this.service.toggle(this.id);
} }

View File

@@ -1,15 +1,27 @@
import { TruncatableAction, TruncatableActionTypes } from './truncatable.actions'; import { TruncatableAction, TruncatableActionTypes } from './truncatable.actions';
/**
* Interface that represents the state of a single truncatable
*/
export interface TruncatableState { export interface TruncatableState {
collapsed: boolean; collapsed: boolean;
} }
/**
* Interface that represents the state of all truncatable
*/
export interface TruncatablesState { export interface TruncatablesState {
[id: string]: TruncatableState [id: string]: TruncatableState
} }
const initialState: TruncatablesState = Object.create(null); const initialState: TruncatablesState = Object.create(null);
/**
* Performs a truncatable action on the current state
* @param {TruncatablesState} state The state before the action is performed
* @param {TruncatableAction} action The action that should be performed
* @returns {TruncatablesState} The state after the action is performed
*/
export function truncatableReducer(state = initialState, action: TruncatableAction): TruncatablesState { export function truncatableReducer(state = initialState, action: TruncatableAction): TruncatablesState {
switch (action.type) { switch (action.type) {

View File

@@ -7,12 +7,20 @@ import { hasValue } from '../empty.util';
const truncatableStateSelector = (state: TruncatablesState) => state.truncatable; const truncatableStateSelector = (state: TruncatablesState) => state.truncatable;
/**
* Service responsible for truncating/clamping text and performing actions on truncatable elements
*/
@Injectable() @Injectable()
export class TruncatableService { export class TruncatableService {
constructor(private store: Store<TruncatablesState>) { constructor(private store: Store<TruncatablesState>) {
} }
/**
* Checks if a trunctable component should currently be collapsed
* @param {string} id The UUID of the truncatable component
* @returns {Observable<boolean>} Emits true if the state in the store is currently collapsed for the given truncatable component
*/
isCollapsed(id: string): Observable<boolean> { isCollapsed(id: string): Observable<boolean> {
return this.store.select(truncatableByIdSelector(id)) return this.store.select(truncatableByIdSelector(id))
.map((object: TruncatableState) => { .map((object: TruncatableState) => {
@@ -24,14 +32,26 @@ export class TruncatableService {
}); });
} }
/**
* Dispatches a toggle action to the store for a given truncatable component
* @param {string} id The identifier of the truncatable for which the action is dispatched
*/
public toggle(id: string): void { public toggle(id: string): void {
this.store.dispatch(new TruncatableToggleAction(id)); this.store.dispatch(new TruncatableToggleAction(id));
} }
/**
* Dispatches a collapse action to the store for a given truncatable component
* @param {string} id The identifier of the truncatable for which the action is dispatched
*/
public collapse(id: string): void { public collapse(id: string): void {
this.store.dispatch(new TruncatableCollapseAction(id)); this.store.dispatch(new TruncatableCollapseAction(id));
} }
/**
* Dispatches an expand action to the store for a given truncatable component
* @param {string} id The identifier of the truncatable for which the action is dispatched
*/
public expand(id: string): void { public expand(id: string): void {
this.store.dispatch(new TruncatableExpandAction(id)); this.store.dispatch(new TruncatableExpandAction(id));
} }

View File

@@ -1,14 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
/**
* Pipe to truncate a value in Angular. (Take a substring, starting at 0)
* Default value: 10
*/
@Pipe({ @Pipe({
name: 'dsCapitalize' name: 'dsCapitalize'
}) })
/**
* Pipe for capizalizing a string
*/
export class CapitalizePipe implements PipeTransform { export class CapitalizePipe implements PipeTransform {
transform(value: string, args: string[]): string { /**
* @param {string} value String to be capitalized
* @returns {string} Capitalized version of the input value
*/
transform(value: string): string {
if (value) { if (value) {
return value.charAt(0).toUpperCase() + value.slice(1); return value.charAt(0).toUpperCase() + value.slice(1);
} }

View File

@@ -1,9 +1,15 @@
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core'; import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core';
@Directive({ @Directive({
selector: '[dsClickOutside]' selector: '[dsClickOutside]'
}) })
/**
* Directive to detect when the users clicks outside of the element the directive was put on
*/
export class ClickOutsideDirective { export class ClickOutsideDirective {
/**
* Emits null when the user clicks outside of the element
*/
@Output() @Output()
public dsClickOutside = new EventEmitter(); public dsClickOutside = new EventEmitter();

View File

@@ -8,22 +8,45 @@ import { Subject } from 'rxjs/Subject';
@Directive({ @Directive({
selector: '[ngModel][dsDebounce]', selector: '[ngModel][dsDebounce]',
}) })
/**
* Directive for setting a debounce time on an input field
* It will emit the input field's value when no changes were made to this value in a given debounce time
*/
export class DebounceDirective implements OnInit, OnDestroy { export class DebounceDirective implements OnInit, OnDestroy {
/**
* Emits a value when nothing has changed in dsDebounce milliseconds
*/
@Output() @Output()
public onDebounce = new EventEmitter<any>(); public onDebounce = new EventEmitter<any>();
/**
* The debounce time in milliseconds
*/
@Input() @Input()
public dsDebounce = 500; public dsDebounce = 500;
/**
* True if no changes have been made to the input field's value
*/
private isFirstChange = true; private isFirstChange = true;
private ngUnsubscribe: Subject<void> = new Subject<void>();
/**
* Subject to unsubscribe from
*/
private subject: Subject<void> = new Subject<void>();
constructor(public model: NgControl) { constructor(public model: NgControl) {
} }
/**
* Start listening to changes of the input field's value changes
* Emit it when the debounceTime is over without new changes
*/
ngOnInit() { ngOnInit() {
this.model.valueChanges this.model.valueChanges
.takeUntil(this.ngUnsubscribe) .takeUntil(this.subject)
.debounceTime(this.dsDebounce) .debounceTime(this.dsDebounce)
.distinctUntilChanged() .distinctUntilChanged()
.subscribe((modelValue) => { .subscribe((modelValue) => {
@@ -35,8 +58,11 @@ export class DebounceDirective implements OnInit, OnDestroy {
}); });
} }
/**
* Close subject
*/
ngOnDestroy() { ngOnDestroy() {
this.ngUnsubscribe.next(); this.subject.next();
this.ngUnsubscribe.complete(); this.subject.complete();
} }
} }

View File

@@ -3,15 +3,36 @@ import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({ @Directive({
selector: '[dsDragClick]' selector: '[dsDragClick]'
}) })
/**
* Directive for preventing drag events being misinterpret as clicks
* The difference is made using the time the mouse button is pushed down
*/
export class DragClickDirective { export class DragClickDirective {
/**
* The start time of the mouse down event in milliseconds
*/
private start; private start;
/**
* Emits a click event when the click is perceived as an actual click and not a drag
*/
@Output() actualClick = new EventEmitter(); @Output() actualClick = new EventEmitter();
/**
* When the mouse button is pushed down, register the start time
* @param event Mouse down event
*/
@HostListener('mousedown', ['$event']) @HostListener('mousedown', ['$event'])
mousedownEvent(event) { mousedownEvent(event) {
this.start = new Date(); this.start = new Date();
} }
/**
* When the mouse button is let go of, check how long if it was down for
* If the mouse button was down for more than 250ms, don't emit a click event
* @param event Mouse down event
*/
@HostListener('mouseup', ['$event']) @HostListener('mouseup', ['$event'])
mouseupEvent(event) { mouseupEvent(event) {
const end: any = new Date(); const end: any = new Date();

View File

@@ -1,7 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'dsEmphasize' }) @Pipe({ name: 'dsEmphasize' })
/**
* Pipe for emphasizing a part of a string by surrounding it with <em> tags
*/
export class EmphasizePipe implements PipeTransform { export class EmphasizePipe implements PipeTransform {
/**
* Characters that should be escaped
*/
specials = [ specials = [
// order matters for these // order matters for these
'-' '-'
@@ -22,14 +28,28 @@ export class EmphasizePipe implements PipeTransform {
, '$' , '$'
, '|' , '|'
]; ];
/**
* Regular expression for escaping the string we're trying to find
*/
regex = RegExp('[' + this.specials.join('\\') + ']', 'g'); regex = RegExp('[' + this.specials.join('\\') + ']', 'g');
/**
*
* @param haystack The string which we want to partly highlight
* @param needle The string that should become emphasized in the haystack string
* @returns {any} Transformed haystack with the needle emphasized
*/
transform(haystack, needle): any { transform(haystack, needle): any {
const escaped = this.escapeRegExp(needle); const escaped = this.escapeRegExp(needle);
const reg = new RegExp(escaped, 'gi'); const reg = new RegExp(escaped, 'gi');
return haystack.replace(reg, '<em>$&</em>'); return haystack.replace(reg, '<em>$&</em>');
} }
/**
*
* @param str Escape special characters in the string we're looking for
* @returns {any} The escaped version of the input string
*/
escapeRegExp(str) { escapeRegExp(str) {
return str.replace(this.regex, '\\$&'); return str.replace(this.regex, '\\$&');
} }

View File

@@ -1,8 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'dsKeys' }) @Pipe({ name: 'dsKeys' })
/**
* Pipe for parsing all values of an enumeration to an array of key-value pairs
*/
export class EnumKeysPipe implements PipeTransform { export class EnumKeysPipe implements PipeTransform {
transform(value, args: string[]): any {
/**
* @param value An enumeration
* @returns {any} Array with all keys and values of the input enumeration
*/
transform(value): any {
const keys = []; const keys = [];
for (const enumMember in value) { for (const enumMember in value) {
if (!isNaN(parseInt(enumMember, 10))) { if (!isNaN(parseInt(enumMember, 10))) {

View File

@@ -1,7 +1,15 @@
import { PipeTransform, Pipe } from '@angular/core'; import { PipeTransform, Pipe } from '@angular/core';
@Pipe({name: 'dsObjectKeys'}) @Pipe({name: 'dsObjectKeys'})
/**
* Pipe for parsing all keys of an object to an array of key-value pairs
*/
export class ObjectKeysPipe implements PipeTransform { export class ObjectKeysPipe implements PipeTransform {
/**
* @param value An object
* @returns {any} Array with all keys the input object
*/
transform(value, args:string[]): any { transform(value, args:string[]): any {
const keys = []; const keys = [];
Object.keys(value).forEach((k) => keys.push(k)); Object.keys(value).forEach((k) => keys.push(k));

View File

@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { SearchService } from '../../+search-page/search-service/search.service'; import { SearchService } from '../../+search-page/search-service/search.service';
import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { ViewModeSwitchComponent } from './view-mode-switch.component';
import { ViewMode } from '../../+search-page/search-options.model';
import { SearchServiceStub } from '../testing/search-service-stub'; import { SearchServiceStub } from '../testing/search-service-stub';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { } class DummyComponent { }

View File

@@ -1,7 +1,7 @@
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ViewMode } from '../../+search-page/search-options.model';
import { SearchService } from './../../+search-page/search-service/search.service'; import { SearchService } from './../../+search-page/search-service/search.service';
import { ViewMode } from '../../core/shared/view-mode.model';
/** /**
* Component to switch between list and grid views. * Component to switch between list and grid views.