mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Refactor search sidebar
This commit is contained in:
@@ -23,7 +23,7 @@ import { SearchConfigurationServiceStub } from '../shared/testing/search-configu
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
|
||||
import { RoleDirective } from '../shared/roles/role.directive';
|
||||
import { RoleService } from '../core/roles/role.service';
|
||||
@@ -108,7 +108,7 @@ describe('MyDSpacePageComponent', () => {
|
||||
})
|
||||
},
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
|
@@ -17,7 +17,7 @@ import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { MyDSpaceResult } from './my-dspace-result.model';
|
||||
@@ -96,7 +96,7 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
viewModeList = [ViewMode.List, ViewMode.Detail];
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SearchSidebarService,
|
||||
private sidebarService: SidebarService,
|
||||
private windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
@@ -36,7 +36,7 @@ export class ConfigurationSearchPageComponent extends SearchPageComponent implem
|
||||
@Input() configuration: string;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
@@ -38,7 +38,7 @@ export class FilteredSearchPageComponent extends SearchPageComponent implements
|
||||
@Input() fixedFilterQuery: string;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
|
@@ -138,7 +138,7 @@ describe('SearchFilterService', () => {
|
||||
service.expand(mockFilterConfig.name);
|
||||
});
|
||||
|
||||
it('SearchSidebarExpandAction should be dispatched to the store', () => {
|
||||
it('SidebarExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterExpandAction(mockFilterConfig.name));
|
||||
});
|
||||
});
|
||||
|
@@ -1,27 +1,15 @@
|
||||
<div class="container">
|
||||
<div class="search-page row">
|
||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky"
|
||||
id="search-sidebar"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div id="search-body"
|
||||
class="row-offcanvas row-offcanvas-left"
|
||||
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||
id="search-sidebar-sm"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
[ngClass]="{'active': !(isSidebarCollapsed$ | async)}">
|
||||
</ds-search-sidebar>
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
@@ -37,7 +25,26 @@
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</ng-template>
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.container {
|
||||
width: 100%;
|
||||
@@ -9,44 +8,3 @@
|
||||
/deep/ .search-controls {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
#search-body {
|
||||
&.row-offcanvas {
|
||||
width: 100%;
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: relative;
|
||||
|
||||
&.row-offcanvas {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right #search-sidebar-sm {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-left #search-sidebar-sm {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
#search-sidebar-sm {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.sidebar-md-sticky {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-sticky;
|
||||
padding-top: $content-spacing;
|
||||
margin-top: -$content-spacing;
|
||||
align-self: flex-start;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
@@ -115,7 +115,7 @@ export function configureSearchComponentTestingModule(compType) {
|
||||
})
|
||||
},
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
@@ -191,34 +191,4 @@ describe('SearchPageComponent', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
(comp as any).isSidebarCollapsed$ = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the sidebar', () => {
|
||||
expect(menu.classList).not.toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
(comp as any).isSidebarCollapsed$ = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -9,7 +9,7 @@ import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
@@ -102,7 +102,7 @@ export class SearchPageComponent implements OnInit {
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
|
@@ -9,8 +9,8 @@ import { ItemSearchResultGridElementComponent } from '../shared/object-grid/sear
|
||||
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
||||
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
|
||||
import { SearchSettingsComponent } from './search-settings/search-settings.component';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { SearchFiltersComponent } from './search-filters/search-filters.component';
|
||||
@@ -36,7 +36,7 @@ import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
|
||||
const effects = [
|
||||
SearchSidebarEffects
|
||||
SidebarEffects
|
||||
];
|
||||
|
||||
const components = [
|
||||
@@ -73,11 +73,11 @@ const components = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EffectsModule.forFeature(effects),
|
||||
CoreModule.forRoot()
|
||||
CoreModule.forRoot(),
|
||||
],
|
||||
declarations: components,
|
||||
providers: [
|
||||
SearchSidebarService,
|
||||
SidebarService,
|
||||
SearchFilterService,
|
||||
SearchFixedFilterService,
|
||||
ConfigurationSearchPageGuard,
|
||||
|
@@ -7,7 +7,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { SearchSidebarService } from '../search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../../shared/sidebar/sidebar.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -65,7 +65,7 @@ describe('SearchSettingsComponent', () => {
|
||||
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
|
@@ -1,47 +0,0 @@
|
||||
import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
|
||||
|
||||
/**
|
||||
* Interface that represents the state of the sidebar
|
||||
*/
|
||||
export interface SearchSidebarState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialState: SearchSidebarState = {
|
||||
sidebarCollapsed: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a search sidebar action on the current state
|
||||
* @param {SearchSidebarState} state The state before the action is performed
|
||||
* @param {SearchSidebarAction} action The action that should be performed
|
||||
* @returns {SearchSidebarState} The state after the action is performed
|
||||
*/
|
||||
export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
|
||||
switch (action.type) {
|
||||
|
||||
case SearchSidebarActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: true
|
||||
});
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: !state.sidebarCollapsed
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,9 +3,9 @@ import * as fromRouter from '@ngrx/router-store';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import {
|
||||
SearchSidebarState,
|
||||
SidebarState,
|
||||
sidebarReducer
|
||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||
} from './shared/sidebar/sidebar.reducer';
|
||||
import {
|
||||
filterReducer,
|
||||
SearchFiltersState
|
||||
@@ -37,7 +37,7 @@ export interface AppState {
|
||||
metadataRegistry: MetadataRegistryState;
|
||||
bitstreamFormats: BitstreamFormatRegistryState;
|
||||
notifications: NotificationsState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchSidebar: SidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState;
|
||||
cssVariables: CSSVariablesState;
|
||||
|
@@ -147,6 +147,7 @@ import { TypedItemSearchResultGridElementComponent } from './object-grid/item-gr
|
||||
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
||||
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';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -233,6 +234,7 @@ const COMPONENTS = [
|
||||
ObjectCollectionComponent,
|
||||
PaginationComponent,
|
||||
SearchFormComponent,
|
||||
PageWithSidebarComponent,
|
||||
ThumbnailComponent,
|
||||
GridThumbnailComponent,
|
||||
UploaderComponent,
|
||||
|
14
src/app/shared/sidebar/page-with-sidebar.component.html
Normal file
14
src/app/shared/sidebar/page-with-sidebar.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="row-with-sidebar row-offcanvas row-offcanvas-left"
|
||||
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||
<div id="{{id}}-sidebar-content"
|
||||
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
|
||||
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
|
||||
</div>
|
||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
52
src/app/shared/sidebar/page-with-sidebar.component.scss
Normal file
52
src/app/shared/sidebar/page-with-sidebar.component.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@include media-breakpoint-down(md) {
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.row-with-sidebar {
|
||||
|
||||
&.row-offcanvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: relative;
|
||||
|
||||
&.row-offcanvas {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right .sidebar-content {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-left .sidebar-content {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.sidebar-content {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-sticky;
|
||||
padding-top: $content-spacing;
|
||||
margin-top: -$content-spacing;
|
||||
align-self: flex-start;
|
||||
display: block;
|
||||
}
|
||||
}
|
75
src/app/shared/sidebar/page-with-sidebar.component.spec.ts
Normal file
75
src/app/shared/sidebar/page-with-sidebar.component.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('PageWithSidebarComponent', () => {
|
||||
let comp:PageWithSidebarComponent;
|
||||
let fixture:ComponentFixture<PageWithSidebarComponent>;
|
||||
|
||||
const sidebarService = {
|
||||
isCollapsed: observableOf(true),
|
||||
collapse: () => this.isCollapsed = observableOf(true),
|
||||
expand: () => this.isCollapsed = observableOf(false)
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||
{
|
||||
isXs: observableOf(true),
|
||||
isSm: observableOf(false),
|
||||
isXsOrSm: observableOf(true)
|
||||
})
|
||||
},
|
||||
],
|
||||
declarations: [PageWithSidebarComponent]
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(PageWithSidebarComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.id = 'mock-id';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||
let menu:HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
|
||||
(comp as any).sidebarService.isCollapsed = observableOf(true);
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the sidebar', () => {
|
||||
expect(menu.classList).not.toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||
let menu:HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
|
||||
(comp as any).sidebarService.isCollapsed = observableOf(false);
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
71
src/app/shared/sidebar/page-with-sidebar.component.ts
Normal file
71
src/app/shared/sidebar/page-with-sidebar.component.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Component, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
import { SidebarService } from './sidebar.service';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { pushInOut } from '../animations/push';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-page-with-sidebar',
|
||||
styleUrls: ['./page-with-sidebar.component.scss'],
|
||||
templateUrl: './page-with-sidebar.component.html',
|
||||
animations: [pushInOut],
|
||||
})
|
||||
export class PageWithSidebarComponent implements OnInit {
|
||||
@Input() id:string;
|
||||
@Input() sidebarContent:TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$:Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$:Observable<boolean>;
|
||||
|
||||
sidebarClasses:Observable<string>;
|
||||
|
||||
constructor(protected sidebarService:SidebarService,
|
||||
protected windowService:HostWindowService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.sidebarClasses = this.isSidebarCollapsed$.pipe(
|
||||
map((isCollapsed) => isCollapsed ? '' : 'active')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed():Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar():void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar():void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
}
|
@@ -3,24 +3,24 @@ import { Observable } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
||||
import { SearchSidebarEffects } from './search-sidebar.effects';
|
||||
import { SidebarCollapseAction } from './sidebar.actions';
|
||||
import { SidebarEffects } from './sidebar-effects.service';
|
||||
|
||||
describe('SearchSidebarEffects', () => {
|
||||
let sidebarEffects: SearchSidebarEffects;
|
||||
describe('SidebarEffects', () => {
|
||||
let sidebarEffects: SidebarEffects;
|
||||
let actions: Observable<any>;
|
||||
const dummyURL = 'http://f4fb15e2-1bd3-4e63-8d0d-486ad8bc714a';
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SearchSidebarEffects,
|
||||
SidebarEffects,
|
||||
provideMockActions(() => actions),
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
sidebarEffects = TestBed.get(SearchSidebarEffects);
|
||||
sidebarEffects = TestBed.get(SidebarEffects);
|
||||
});
|
||||
|
||||
describe('routeChange$', () => {
|
||||
@@ -28,7 +28,7 @@ describe('SearchSidebarEffects', () => {
|
||||
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action to a new route', () => {
|
||||
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION, payload: {routerState: {url: dummyURL}} } });
|
||||
|
||||
const expected = cold('--b-', { b: new SearchSidebarCollapseAction() });
|
||||
const expected = cold('--b-', { b: new SidebarCollapseAction() });
|
||||
|
||||
expect(sidebarEffects.routeChange$).toBeObservable(expected);
|
||||
});
|
@@ -3,14 +3,14 @@ import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions, ofType } from '@ngrx/effects'
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
||||
import { SidebarCollapseAction } from './sidebar.actions';
|
||||
import { URLBaser } from '../../core/url-baser/url-baser';
|
||||
|
||||
/**
|
||||
* Makes sure that if the user navigates to another route, the sidebar is collapsed
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchSidebarEffects {
|
||||
export class SidebarEffects {
|
||||
private previousPath: string;
|
||||
@Effect() routeChange$ = this.actions$
|
||||
.pipe(
|
||||
@@ -19,7 +19,7 @@ export class SearchSidebarEffects {
|
||||
tap((action) => {
|
||||
this.previousPath = this.getBaseUrl(action)
|
||||
}),
|
||||
map(() => new SearchSidebarCollapseAction())
|
||||
map(() => new SidebarCollapseAction())
|
||||
);
|
||||
|
||||
constructor(private actions$: Actions) {
|
@@ -1,6 +1,6 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { type } from '../ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
@@ -10,32 +10,32 @@ import { type } from '../../shared/ngrx/type';
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const SearchSidebarActionTypes = {
|
||||
COLLAPSE: type('dspace/search-sidebar/COLLAPSE'),
|
||||
EXPAND: type('dspace/search-sidebar/EXPAND'),
|
||||
TOGGLE: type('dspace/search-sidebar/TOGGLE')
|
||||
export const SidebarActionTypes = {
|
||||
COLLAPSE: type('dspace/sidebar/COLLAPSE'),
|
||||
EXPAND: type('dspace/sidebar/EXPAND'),
|
||||
TOGGLE: type('dspace/sidebar/TOGGLE')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to collapse the sidebar
|
||||
*/
|
||||
export class SearchSidebarCollapseAction implements Action {
|
||||
type = SearchSidebarActionTypes.COLLAPSE;
|
||||
export class SidebarCollapseAction implements Action {
|
||||
type = SidebarActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to expand the sidebar
|
||||
*/
|
||||
export class SearchSidebarExpandAction implements Action {
|
||||
type = SearchSidebarActionTypes.EXPAND;
|
||||
export class SidebarExpandAction implements Action {
|
||||
type = SidebarActionTypes.EXPAND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to collapse the sidebar when it's expanded and expand it when it's collapsed
|
||||
*/
|
||||
export class SearchSidebarToggleAction implements Action {
|
||||
type = SearchSidebarActionTypes.TOGGLE;
|
||||
export class SidebarToggleAction implements Action {
|
||||
type = SidebarActionTypes.TOGGLE;
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SearchSidebarToggleAction implements Action {
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
*/
|
||||
export type SearchSidebarAction
|
||||
= SearchSidebarCollapseAction
|
||||
| SearchSidebarExpandAction
|
||||
| SearchSidebarToggleAction
|
||||
export type SidebarAction
|
||||
= SidebarCollapseAction
|
||||
| SidebarExpandAction
|
||||
| SidebarToggleAction
|
@@ -1,12 +1,12 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { sidebarReducer } from './search-sidebar.reducer';
|
||||
import { sidebarReducer } from './sidebar.reducer';
|
||||
import {
|
||||
SearchSidebarCollapseAction, SearchSidebarExpandAction,
|
||||
SearchSidebarToggleAction
|
||||
} from './search-sidebar.actions';
|
||||
SidebarCollapseAction, SidebarExpandAction,
|
||||
SidebarToggleAction
|
||||
} from './sidebar.actions';
|
||||
|
||||
class NullAction extends SearchSidebarCollapseAction {
|
||||
class NullAction extends SidebarCollapseAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
@@ -34,7 +34,7 @@ describe('sidebarReducer', () => {
|
||||
|
||||
it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => {
|
||||
const state = { sidebarCollapsed: false };
|
||||
const action = new SearchSidebarCollapseAction();
|
||||
const action = new SidebarCollapseAction();
|
||||
const newState = sidebarReducer(state, action);
|
||||
|
||||
expect(newState.sidebarCollapsed).toEqual(true);
|
||||
@@ -44,7 +44,7 @@ describe('sidebarReducer', () => {
|
||||
const state = { sidebarCollapsed: false };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarCollapseAction();
|
||||
const action = new SidebarCollapseAction();
|
||||
sidebarReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
@@ -53,7 +53,7 @@ describe('sidebarReducer', () => {
|
||||
|
||||
it('should set sidebarCollapsed to false in response to the EXPAND action', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
const action = new SearchSidebarExpandAction();
|
||||
const action = new SidebarExpandAction();
|
||||
const newState = sidebarReducer(state, action);
|
||||
|
||||
expect(newState.sidebarCollapsed).toEqual(false);
|
||||
@@ -63,13 +63,13 @@ describe('sidebarReducer', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarExpandAction();
|
||||
const action = new SidebarExpandAction();
|
||||
sidebarReducer(state, action);
|
||||
});
|
||||
|
||||
it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => {
|
||||
const state1 = { sidebarCollapsed: true };
|
||||
const action = new SearchSidebarToggleAction();
|
||||
const action = new SidebarToggleAction();
|
||||
|
||||
const state2 = sidebarReducer(state1, action);
|
||||
const state3 = sidebarReducer(state2, action);
|
||||
@@ -82,7 +82,7 @@ describe('sidebarReducer', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarToggleAction();
|
||||
const action = new SidebarToggleAction();
|
||||
sidebarReducer(state, action);
|
||||
});
|
||||
|
47
src/app/shared/sidebar/sidebar.reducer.ts
Normal file
47
src/app/shared/sidebar/sidebar.reducer.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { SidebarAction, SidebarActionTypes } from './sidebar.actions';
|
||||
|
||||
/**
|
||||
* Interface that represents the state of the sidebar
|
||||
*/
|
||||
export interface SidebarState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialState: SidebarState = {
|
||||
sidebarCollapsed: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a search 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
|
||||
*/
|
||||
export function sidebarReducer(state = initialState, action: SidebarAction): SidebarState {
|
||||
switch (action.type) {
|
||||
|
||||
case SidebarActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: true
|
||||
});
|
||||
}
|
||||
|
||||
case SidebarActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
case SidebarActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: !state.sidebarCollapsed
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { SearchSidebarService } from './search-sidebar.service';
|
||||
import { SidebarService } from './sidebar.service';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { SidebarCollapseAction, SidebarExpandAction } from './sidebar.actions';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
|
||||
describe('SearchSidebarService', () => {
|
||||
let service: SearchSidebarService;
|
||||
describe('SidebarService', () => {
|
||||
let service: SidebarService;
|
||||
const store: Store<AppState> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
@@ -35,7 +35,7 @@ describe('SearchSidebarService', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
service = new SearchSidebarService(store, windowService);
|
||||
service = new SidebarService(store, windowService);
|
||||
}) ;
|
||||
|
||||
describe('when the collapse method is triggered', () => {
|
||||
@@ -43,8 +43,8 @@ describe('SearchSidebarService', () => {
|
||||
service.collapse();
|
||||
});
|
||||
|
||||
it('SearchSidebarCollapseAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarCollapseAction());
|
||||
it('SidebarCollapseAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SidebarCollapseAction());
|
||||
});
|
||||
|
||||
});
|
||||
@@ -54,8 +54,8 @@ describe('SearchSidebarService', () => {
|
||||
service.expand();
|
||||
});
|
||||
|
||||
it('SearchSidebarExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarExpandAction());
|
||||
it('SidebarExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SidebarExpandAction());
|
||||
});
|
||||
});
|
||||
|
@@ -1,20 +1,20 @@
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SearchSidebarState } from './search-sidebar.reducer';
|
||||
import { SidebarState } from './sidebar.reducer';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
|
||||
import { SidebarCollapseAction, SidebarExpandAction } from './sidebar.actions';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
const sidebarStateSelector = (state: AppState) => state.searchSidebar;
|
||||
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed);
|
||||
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SidebarState) => sidebar.sidebarCollapsed);
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with the search sidebar
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchSidebarService {
|
||||
export class SidebarService {
|
||||
/**
|
||||
* Emits true is the current screen size is mobile
|
||||
*/
|
||||
@@ -47,13 +47,13 @@ export class SearchSidebarService {
|
||||
* Dispatches a collapse action to the store
|
||||
*/
|
||||
public collapse(): void {
|
||||
this.store.dispatch(new SearchSidebarCollapseAction());
|
||||
this.store.dispatch(new SidebarCollapseAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an expand action to the store
|
||||
*/
|
||||
public expand(): void {
|
||||
this.store.dispatch(new SearchSidebarExpandAction());
|
||||
this.store.dispatch(new SidebarExpandAction());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user