diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-filter.component.html
index a1758d7339..a03a0de451 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.html
@@ -1,7 +1,18 @@
-
{{'search.filters.filter.' + filter.name + '.head'| translate}}
-
-
-
+
+
+ {{'search.filters.filter.' + filter.name + '.head'| translate}}
+
+
+
+
+
+
+
+
diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts
index a500cc42d7..8d583a78af 100644
--- a/src/app/+search-page/search-page.module.ts
+++ b/src/app/+search-page/search-page.module.ts
@@ -34,6 +34,7 @@ import { SearchLabelComponent } from './search-labels/search-label/search-label.
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { FilteredSearchPageComponent } from './filtered-search-page.component';
+import { SidebarFilterService } from "../shared/sidebar/filter/sidebar-filter.service";
const effects = [
SidebarEffects
@@ -78,6 +79,7 @@ const components = [
declarations: components,
providers: [
SidebarService,
+ SidebarFilterService,
SearchFilterService,
SearchFixedFilterService,
ConfigurationSearchPageGuard,
diff --git a/src/app/+search-page/search-settings/search-settings.component.html b/src/app/+search-page/search-settings/search-settings.component.html
index d693196dae..d8878982b4 100644
--- a/src/app/+search-page/search-settings/search-settings.component.html
+++ b/src/app/+search-page/search-settings/search-settings.component.html
@@ -1,24 +1,32 @@
- {{ 'search.sidebar.settings.title' | translate}}
-
-
{{ 'search.sidebar.settings.sort-by' | translate}}
-
-
+ {{ 'search.sidebar.settings.title' | translate}}
-
-
{{ 'search.sidebar.settings.rpp' | translate}}
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index fbdce542a8..f9dc813722 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -1,4 +1,4 @@
-import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
+import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
@@ -6,6 +6,10 @@ import {
SidebarState,
sidebarReducer
} from './shared/sidebar/sidebar.reducer';
+import {
+ SidebarFilterState,
+ sidebarFilterReducer, SidebarFiltersState
+} from './shared/sidebar/filter/sidebar-filter.reducer';
import {
filterReducer,
SearchFiltersState
@@ -37,7 +41,8 @@ export interface AppState {
metadataRegistry: MetadataRegistryState;
bitstreamFormats: BitstreamFormatRegistryState;
notifications: NotificationsState;
- searchSidebar: SidebarState;
+ sidebar: SidebarState;
+ sidebarFilter: SidebarFiltersState;
searchFilter: SearchFiltersState;
truncatable: TruncatablesState;
cssVariables: CSSVariablesState;
@@ -53,7 +58,8 @@ export const appReducers: ActionReducerMap = {
metadataRegistry: metadataRegistryReducer,
bitstreamFormats: bitstreamFormatReducer,
notifications: notificationsReducer,
- searchSidebar: sidebarReducer,
+ sidebar: sidebarReducer,
+ sidebarFilter: sidebarFilterReducer,
searchFilter: filterReducer,
truncatable: truncatableReducer,
cssVariables: cssVariablesReducer,
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 5a1c2de26f..0d427be49c 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -148,6 +148,9 @@ import { PublicationGridElementComponent } from './object-grid/item-grid-element
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component';
+import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component';
+import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component';
+import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -235,6 +238,9 @@ const COMPONENTS = [
PaginationComponent,
SearchFormComponent,
PageWithSidebarComponent,
+ SidebarDropdownComponent,
+ SidebarFilterComponent,
+ SidebarFilterSelectedOptionComponent,
ThumbnailComponent,
GridThumbnailComponent,
UploaderComponent,
diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html
new file mode 100644
index 0000000000..bbe0b93566
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss
new file mode 100644
index 0000000000..b4e9cd340c
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss
@@ -0,0 +1,11 @@
+a {
+ color: $body-color;
+
+ &:hover, &focus {
+ text-decoration: none;
+ }
+
+ span.badge {
+ vertical-align: text-top;
+ }
+}
diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts
new file mode 100644
index 0000000000..5c80a9cd87
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts
@@ -0,0 +1,15 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+@Component({
+ selector: 'ds-sidebar-filter-selected-option',
+ styleUrls: ['./sidebar-filter-selected-option.component.scss'],
+ templateUrl: './sidebar-filter-selected-option.component.html',
+})
+
+/**
+ * Represents a single selected option in a sidebar filter
+ */
+export class SidebarFilterSelectedOptionComponent {
+ @Input() label:string;
+ @Output() click:EventEmitter = new EventEmitter();
+}
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts
new file mode 100644
index 0000000000..2391274489
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts
@@ -0,0 +1,74 @@
+import { Action } from '@ngrx/store';
+
+import { type } from '../../ngrx/type';
+
+/**
+ * For each action type in an action group, make a simple
+ * enum object for all of this group's action types.
+ *
+ * The 'type' utility function coerces strings into string
+ * literal types and runs a simple check to guarantee all
+ * action types in the application are unique.
+ */
+export const SidebarFilterActionTypes = {
+ INITIALIZE: type('dspace/sidebar-filter/INITIALIZE'),
+ COLLAPSE: type('dspace/sidebar-filter/COLLAPSE'),
+ EXPAND: type('dspace/sidebar-filter/EXPAND'),
+ TOGGLE: type('dspace/sidebar-filter/TOGGLE'),
+};
+
+export class SidebarFilterAction implements Action {
+ /**
+ * Name of the filter the action is performed on, used to identify the filter
+ */
+ filterName: string;
+
+ /**
+ * Type of action that will be performed
+ */
+ type;
+
+ /**
+ * Initialize with the filter's name
+ * @param {string} name of the filter
+ */
+ constructor(name: string) {
+ this.filterName = name;
+ }
+}
+
+/* tslint:disable:max-classes-per-file */
+/**
+ * Used to initialize a filter
+ */
+export class FilterInitializeAction extends SidebarFilterAction {
+ type = SidebarFilterActionTypes.INITIALIZE;
+ initiallyExpanded;
+
+ constructor(name:string, initiallyExpanded:boolean) {
+ super(name);
+ this.initiallyExpanded = initiallyExpanded;
+ }
+}
+
+/**
+ * Used to collapse a filter
+ */
+export class FilterCollapseAction extends SidebarFilterAction {
+ type = SidebarFilterActionTypes.COLLAPSE;
+}
+
+/**
+ * Used to expand a filter
+ */
+export class FilterExpandAction extends SidebarFilterAction {
+ type = SidebarFilterActionTypes.EXPAND;
+}
+
+/**
+ * Used to collapse a filter when it's expanded and expand it when it's collapsed
+ */
+export class FilterToggleAction extends SidebarFilterAction {
+ type = SidebarFilterActionTypes.TOGGLE;
+}
+/* tslint:enable:max-classes-per-file */
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.html b/src/app/shared/sidebar/filter/sidebar-filter.component.html
new file mode 100644
index 0000000000..b0209d9900
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.component.html
@@ -0,0 +1,24 @@
+
+
+
+ {{ label | translate }}
+
+
+
+
+
+
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.scss b/src/app/shared/sidebar/filter/sidebar-filter.component.scss
new file mode 100644
index 0000000000..68949f3450
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.component.scss
@@ -0,0 +1,12 @@
+:host .facet-filter {
+ border: 1px solid map-get($theme-colors, light);
+ cursor: pointer;
+
+ .sidebar-filter-wrapper.closed {
+ overflow: hidden;
+ }
+
+ .filter-toggle {
+ line-height: $line-height-base;
+ }
+}
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.ts b/src/app/shared/sidebar/filter/sidebar-filter.component.ts
new file mode 100644
index 0000000000..4d5f41d971
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.component.ts
@@ -0,0 +1,84 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { Observable } from 'rxjs';
+import { SidebarFilterService } from './sidebar-filter.service';
+import { slide } from '../../animations/slide';
+
+@Component({
+ selector: 'ds-sidebar-filter',
+ styleUrls: ['./sidebar-filter.component.scss'],
+ templateUrl: './sidebar-filter.component.html',
+ animations: [slide],
+})
+export class SidebarFilterComponent implements OnInit {
+
+ @Input() name:string;
+ @Input() type:string;
+ @Input() label:string;
+ @Input() expanded = true;
+ @Input() selectedValues:Observable;
+ @Output() submitValue:EventEmitter = new EventEmitter();
+ @Output() removeValue:EventEmitter = new EventEmitter();
+
+ /**
+ * True when the filter is 100% collapsed in the UI
+ */
+ closed = true;
+
+ /**
+ * Emits true when the filter is currently collapsed in the store
+ */
+ collapsed$:Observable;
+
+ constructor(
+ protected filterService:SidebarFilterService
+ ) {
+ }
+
+ /**
+ * Changes the state for this filter to collapsed when it's expanded and to expanded it when it's collapsed
+ */
+ toggle() {
+ this.filterService.toggle(this.name);
+ }
+
+ /**
+ * Method to change this.collapsed to false when the slide animation ends and is sliding open
+ * @param event The animation event
+ */
+ finishSlide(event:any):void {
+ if (event.fromState === 'collapsed') {
+ this.closed = false;
+ }
+ }
+
+ /**
+ * Method to change this.collapsed to true when the slide animation starts and is sliding closed
+ * @param event The animation event
+ */
+ startSlide(event:any):void {
+ if (event.toState === 'collapsed') {
+ this.closed = true;
+ }
+ }
+
+ ngOnInit():void {
+ this.initializeFilter();
+ this.collapsed$ = this.isCollapsed();
+ }
+
+ /**
+ * Sets the initial state of the filter
+ */
+ initializeFilter() {
+ this.filterService.initializeFilter(this.name, this.expanded);
+ }
+
+ /**
+ * Checks if the filter is currently collapsed
+ * @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded
+ */
+ private isCollapsed():Observable {
+ return this.filterService.isCollapsed(this.name);
+ }
+
+}
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts
new file mode 100644
index 0000000000..d25737eaa9
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts
@@ -0,0 +1,70 @@
+import {
+ FilterInitializeAction,
+ SidebarFilterAction,
+ SidebarFilterActionTypes
+} from './sidebar-filter.actions';
+
+/**
+ * Interface that represents the state for a single filters
+ */
+export interface SidebarFilterState {
+ filterCollapsed:boolean,
+}
+
+/**
+ * Interface that represents the state for all available filters
+ */
+export interface SidebarFiltersState {
+ [name:string]:SidebarFilterState
+}
+
+const initialState:SidebarFiltersState = Object.create(null);
+
+/**
+ * Performs a filter action on the current state
+ * @param {SidebarFiltersState} state The state before the action is performed
+ * @param {SidebarFilterAction} action The action that should be performed
+ * @returns {SidebarFiltersState} The state after the action is performed
+ */
+export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState {
+
+ switch (action.type) {
+
+ case SidebarFilterActionTypes.INITIALIZE: {
+ const initAction = (action as FilterInitializeAction);
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: !initAction.initiallyExpanded,
+ }
+ });
+ }
+
+ case SidebarFilterActionTypes.COLLAPSE: {
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: true,
+ }
+ });
+ }
+
+ case SidebarFilterActionTypes.EXPAND: {
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: false,
+ }
+ });
+ }
+
+ case SidebarFilterActionTypes.TOGGLE: {
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: !state[action.filterName].filterCollapsed,
+ }
+ });
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts
new file mode 100644
index 0000000000..b08c7a8b73
--- /dev/null
+++ b/src/app/shared/sidebar/filter/sidebar-filter.service.ts
@@ -0,0 +1,87 @@
+import { Injectable } from '@angular/core';
+import {
+ FilterCollapseAction,
+ FilterExpandAction, FilterInitializeAction,
+ FilterToggleAction
+} from './sidebar-filter.actions';
+import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { SidebarFiltersState, SidebarFilterState } from './sidebar-filter.reducer';
+import { Observable } from 'rxjs';
+import { distinctUntilChanged, map } from 'rxjs/operators';
+import { hasValue } from '../../empty.util';
+
+@Injectable()
+export class SidebarFilterService {
+
+ constructor(private store:Store) {
+ }
+
+ /**
+ * Dispatches an initialize action to the store for a given filter
+ * @param {string} filter The filter for which the action is dispatched
+ * @param {boolean} expanded If the filter should be open from the start
+ */
+ public initializeFilter(filter:string, expanded:boolean):void {
+ this.store.dispatch(new FilterInitializeAction(filter, expanded));
+ }
+
+ /**
+ * 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 FilterCollapseAction(filterName));
+ }
+
+ /**
+ * Dispatches an 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 FilterExpandAction(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 FilterToggleAction(filterName));
+ }
+
+ /**
+ * 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} Emits the current collapsed state of the given filter, if it's unavailable, return false
+ */
+ isCollapsed(filterName:string):Observable {
+ return this.store.pipe(
+ select(filterByNameSelector(filterName)),
+ map((object:SidebarFilterState) => {
+ if (object) {
+ return object.filterCollapsed;
+ } else {
+ return false;
+ }
+ }),
+ distinctUntilChanged()
+ );
+ }
+
+}
+
+const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter;
+
+function filterByNameSelector(name:string):MemoizedSelector {
+ return keySelector(name);
+}
+
+export function keySelector(key:string):MemoizedSelector {
+ return createSelector(filterStateSelector, (state:SidebarFilterState) => {
+ if (hasValue(state)) {
+ return state[key];
+ } else {
+ return undefined;
+ }
+ });
+}
diff --git a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts
index dfe035d2be..77f59090ab 100644
--- a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts
+++ b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts
@@ -2,8 +2,8 @@ import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PageWithSidebarComponent } from './page-with-sidebar.component';
-import { SidebarService } from './sidebar/sidebar.service';
-import { HostWindowService } from '../shared/host-window.service';
+import { SidebarService } from './sidebar.service';
+import { HostWindowService } from '../host-window.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('PageWithSidebarComponent', () => {
diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.html b/src/app/shared/sidebar/sidebar-dropdown.component.html
new file mode 100644
index 0000000000..0c2a1c05d2
--- /dev/null
+++ b/src/app/shared/sidebar/sidebar-dropdown.component.html
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.scss b/src/app/shared/sidebar/sidebar-dropdown.component.scss
new file mode 100644
index 0000000000..1c025095dd
--- /dev/null
+++ b/src/app/shared/sidebar/sidebar-dropdown.component.scss
@@ -0,0 +1,3 @@
+.setting-option {
+ border: 1px solid map-get($theme-colors, light);
+}
diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.ts b/src/app/shared/sidebar/sidebar-dropdown.component.ts
new file mode 100644
index 0000000000..d4c996157c
--- /dev/null
+++ b/src/app/shared/sidebar/sidebar-dropdown.component.ts
@@ -0,0 +1,12 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+@Component({
+ selector: 'ds-sidebar-dropdown',
+ styleUrls: ['./sidebar-dropdown.component.scss'],
+ templateUrl: './sidebar-dropdown.component.html',
+})
+export class SidebarDropdownComponent {
+ @Input() id:string;
+ @Input() label:string;
+ @Output() change:EventEmitter = new EventEmitter();
+}
diff --git a/src/app/shared/sidebar/sidebar.reducer.ts b/src/app/shared/sidebar/sidebar.reducer.ts
index 3c3dc63b0d..05e7d38d48 100644
--- a/src/app/shared/sidebar/sidebar.reducer.ts
+++ b/src/app/shared/sidebar/sidebar.reducer.ts
@@ -12,7 +12,7 @@ const initialState: SidebarState = {
};
/**
- * Performs a search sidebar action on the current state
+ * Performs a sidebar action on the current state
* @param {SidebarState} state The state before the action is performed
* @param {SidebarAction} action The action that should be performed
* @returns {SidebarState} The state after the action is performed
diff --git a/src/app/shared/sidebar/sidebar.service.ts b/src/app/shared/sidebar/sidebar.service.ts
index 2e3303887b..125fb5b629 100644
--- a/src/app/shared/sidebar/sidebar.service.ts
+++ b/src/app/shared/sidebar/sidebar.service.ts
@@ -7,7 +7,7 @@ import { AppState } from '../../app.reducer';
import { HostWindowService } from '../host-window.service';
import { map } from 'rxjs/operators';
-const sidebarStateSelector = (state: AppState) => state.searchSidebar;
+const sidebarStateSelector = (state: AppState) => state.sidebar;
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SidebarState) => sidebar.sidebarCollapsed);
/**