57053: progress menu bar

This commit is contained in:
lotte
2018-11-16 17:00:06 +01:00
parent 0c4784346b
commit cd5c7b72c2
22 changed files with 386 additions and 127 deletions

View File

@@ -10,52 +10,81 @@ import { type } from '../../shared/ngrx/type';
* literal types and runs a simple check to guarantee all * literal types and runs a simple check to guarantee all
* action types in the application are unique. * action types in the application are unique.
*/ */
export const AdminSidebarSectionActionTypes = { export const AdminSidebarActionTypes = {
COLLAPSE: type('dspace/admin-sidebar-section/COLLAPSE'), SECTION_COLLAPSE: type('dspace/admin-sidebar/SECTION_COLLAPSE'),
EXPAND: type('dspace/admin-sidebar-sectio/EXPAND'), SECTION_EXPAND: type('dspace/admin-sidebar/SECTION_EXPAND'),
TOGGLE: type('dspace/admin-sidebar-sectio/TOGGLE'), SECTION_TOGGLE: type('dspace/admin-sidebar/SECTION_TOGGLE'),
COLLAPSE: type('dspace/admin-sidebar/COLLAPSE'),
EXPAND: type('dspace/admin-sidebar/EXPAND'),
TOGGLE: type('dspace/admin-sidebar/TOGGLE'),
}; };
export class AdminSidebarSectionAction implements Action { /* tslint:disable:max-classes-per-file */
export class AdminSidebarAction implements Action {
/**
* Type of action that will be performed
*/
type;
}
export class AdminSidebarSectionAction extends AdminSidebarAction {
/** /**
* Name of the section the action is performed on, used to identify the section * Name of the section the action is performed on, used to identify the section
*/ */
sectionName: string; sectionName: string;
/**
* Type of action that will be performed
*/
type;
/** /**
* Initialize with the section's name * Initialize with the section's name
* @param {string} name of the section * @param {string} name of the section
*/ */
constructor(name: string) { constructor(name: string) {
super();
this.sectionName = name; this.sectionName = name;
} }
} }
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
/**
* Used to collapse the sidebar
*/
export class AdminSidebarCollapseAction extends AdminSidebarAction {
type = AdminSidebarActionTypes.COLLAPSE;
}
/**
* Used to expand the sidebar
*/
export class AdminSidebarExpandAction extends AdminSidebarAction {
type = AdminSidebarActionTypes.EXPAND;
}
/**
* Used to collapse the sidebar when it's expanded and expand it when it's collapsed
*/
export class AdminSidebarToggleAction extends AdminSidebarAction {
type = AdminSidebarActionTypes.TOGGLE;
}
/** /**
* Used to collapse a section * Used to collapse a section
*/ */
export class AdminSidebarSectionCollapseAction extends AdminSidebarSectionAction { export class AdminSidebarSectionCollapseAction extends AdminSidebarSectionAction {
type = AdminSidebarSectionActionTypes.COLLAPSE; type = AdminSidebarActionTypes.SECTION_COLLAPSE;
} }
/** /**
* Used to expand a section * Used to expand a section
*/ */
export class AdminSidebarSectionExpandAction extends AdminSidebarSectionAction { export class AdminSidebarSectionExpandAction extends AdminSidebarSectionAction {
type = AdminSidebarSectionActionTypes.EXPAND; type = AdminSidebarActionTypes.SECTION_EXPAND;
} }
/** /**
* Used to collapse a section when it's expanded and expand it when it's collapsed * Used to collapse a section when it's expanded and expand it when it's collapsed
*/ */
export class AdminSidebarSectionToggleAction extends AdminSidebarSectionAction { export class AdminSidebarSectionToggleAction extends AdminSidebarSectionAction {
type = AdminSidebarSectionActionTypes.TOGGLE; type = AdminSidebarActionTypes.SECTION_TOGGLE;
} }
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */

View File

@@ -1,17 +1,21 @@
<nav class="navbar navbar-dark bg-dark p-0"> <nav class="navbar navbar-dark bg-dark p-0" [ngClass]="{'active': (sidebarCollapsed | async)}"
[@slideSidebar]="{
value: ((sidebarCollapsed | async) ? 'collapsed' : 'expanded'),
params: {sidebarWidth: (sidebarWidth | async)}
}">
<div class="sidebar-top-level-items"> <div class="sidebar-top-level-items">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="admin-menu-header"> <li class="admin-menu-header">
<a class="shortcuts-tree navbar-brand" href="#"> <a class="shortcuts-tree navbar-brand" href="#">
<img class="admin-logo mr-2" src="assets/images/dspace-logo-mini.svg"> <img class="admin-logo mr-2" src="assets/images/dspace-logo-mini.svg">
<h4 class="nav-item-name">Admin</h4> <h4 class="section-header-text">Admin</h4>
</a> </a>
</li> </li>
<li [ngClass]="{'active': (active('new') | async)}"> <li [ngClass]="{'active': (active('new') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'new')"> (click)="toggleSection($event, 'new')">
<i class="fas fa-plus-circle fa-fw"></i> <i class="fas fa-plus-circle fa-fw"></i>
New <span class="section-header-text">New</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('new') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('new') | async)}"></i>
</a> </a>
@@ -24,9 +28,9 @@
</li> </li>
<li [ngClass]="{'active': (active('edit') | async)}"> <li [ngClass]="{'active': (active('edit') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'edit')"> (click)="toggleSection($event, 'edit')">
<i class="fas fa-pencil-alt fa-fw"></i> <i class="fas fa-pencil-alt fa-fw"></i>
Edit <span class="section-header-text">Edit</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('edit') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('edit') | async)}"></i>
</a> </a>
@@ -38,9 +42,9 @@
</li> </li>
<li [ngClass]="{'active': (active('import') | async)}"> <li [ngClass]="{'active': (active('import') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'import')"> (click)="toggleSection($event, 'import')">
<i class="fas fa-arrow-circle-up fa-fw"></i> <i class="fas fa-arrow-circle-up fa-fw"></i>
Import <span class="section-header-text">Import</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('import') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('import') | async)}"></i>
</a> </a>
@@ -51,9 +55,9 @@
</li> </li>
<li [ngClass]="{'active': (active('export') | async)}"> <li [ngClass]="{'active': (active('export') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'export')"> (click)="toggleSection($event, 'export')">
<i class="fas fa-arrow-circle-down fa-fw"></i> <i class="fas fa-arrow-circle-down fa-fw"></i>
Export <span class="section-header-text">Export</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('export') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('export') | async)}"></i>
</a> </a>
@@ -66,9 +70,9 @@
</li> </li>
<li [ngClass]="{'active': (active('access_control') | async)}"> <li [ngClass]="{'active': (active('access_control') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'access_control')"> (click)="toggleSection($event, 'access_control')">
<i class="fas fa-key fa-fw"></i> <i class="fas fa-key fa-fw"></i>
Access Control <span class="section-header-text">Access Control</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('access_control') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('access_control') | async)}"></i>
</a> </a>
@@ -81,9 +85,9 @@
</li> </li>
<li [ngClass]="{'active': (active('find') | async)}"> <li [ngClass]="{'active': (active('find') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'find')"> (click)="toggleSection($event, 'find')">
<i class="fas fa-search fa-fw"></i> <i class="fas fa-search fa-fw"></i>
Find <span class="section-header-text">Find</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('find') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('find') | async)}"></i>
</a> </a>
@@ -95,9 +99,9 @@
</li> </li>
<li [ngClass]="{'active': (active('registries') | async)}"> <li [ngClass]="{'active': (active('registries') | async)}">
<a class="nav-item nav-link shortcuts-tree" href="#" <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event, 'registries')"> (click)="toggleSection($event, 'registries')">
<i class="fas fa-list fa-fw"></i> <i class="fas fa-list fa-fw"></i>
Registries <span class="section-header-text">Registries</span>
<i class="fas fa-chevron-right fa-pull-right fa-xxs" <i class="fas fa-chevron-right fa-pull-right fa-xxs"
[ngClass]="{'fa-rotate-90': (active('registries') | async)}"></i> [ngClass]="{'fa-rotate-90': (active('registries') | async)}"></i>
</a> </a>
@@ -109,27 +113,28 @@
<li> <li>
<a class="nav-item nav-link shortcuts-tree" href="#"> <a class="nav-item nav-link shortcuts-tree" href="#">
<i class="fas fa-filter fa-fw"></i> <i class="fas fa-filter fa-fw"></i>
Curation Tasks <span class="section-header-text">Curation Tasks</span>
</a> </a>
</li> </li>
<li> <li>
<a class="nav-item nav-link shortcuts-tree" href="#"> <a class="nav-item nav-link shortcuts-tree" href="#">
<i class="fas fa-chart-bar fa-fw"></i> <i class="fas fa-chart-bar fa-fw"></i>
Statistics <span class="section-header-text">Statistics</span>
</a> </a>
</li> </li>
<li> <li>
<a class="nav-item nav-link shortcuts-tree" href="#"> <a class="nav-item nav-link shortcuts-tree" href="#">
<i class="fas fa-cogs fa-fw"></i> <i class="fas fa-cogs fa-fw"></i>
Control Panel <span class="section-header-text">Control Panel</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="navbar-nav"> <div class="navbar-nav">
<a class="nav-item nav-link shortcuts-tree" href="#"> <a class="nav-item nav-link shortcuts-tree" href="#"
(click)="toggle($event)">
<i class="fas fa-fw fa-angle-double-right"></i> <i class="fas fa-fw fa-angle-double-right"></i>
Collapse <span class="section-header-text">Collapse</span>
</a> </a>
</div> </div>
</nav> </nav>

View File

@@ -1,21 +1,36 @@
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
:host { :host {
position: fixed; position: sticky;
nav { left: 0;
top: 0;
height: 100vh; height: 100vh;
flex: 1 1 auto;
nav {
height: 100%;
flex-direction: column; flex-direction: column;
>div.sidebar-top-level-items { > div {
width: 100%;
&.sidebar-top-level-items {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
}
.section-header-text {
display: inline;
}
.navbar-nav { .navbar-nav {
min-width: $admin-sidebar-width; > li, > a {
> * {
padding: $spacer/2 $spacer; padding: $spacer/2 $spacer;
&.active { &.active {
background-color: $admin-sidebar-dark; background-color: $admin-sidebar-dark;
} }
.fa-fw {
text-align: left;
width: 1.75em;
}
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
list-style: disc; list-style: disc;
@@ -28,14 +43,16 @@
.admin-menu-header { .admin-menu-header {
background-color: $admin-sidebar-dark; background-color: $admin-sidebar-dark;
img { img {
height: 1em; height: 1em;
vertical-align: baseline; vertical-align: baseline;
} }
h4 {
display: inline;
} }
} }
&.active {
.section-header-text, .fa-chevron-right {
display: none;
}
} }
} }
} }

View File

@@ -1,34 +1,41 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { MemoizedSelector, select, Store } from '@ngrx/store';
import { AdminSidebarSectionsState, AdminSidebarSectionState } from './admin-sidebar.reducer'; import { AdminSidebarSectionState, AdminSidebarState, } from './admin-sidebar.reducer';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AdminSidebarSectionToggleAction } from './admin-sidebar.actions'; import { AdminSidebarSectionToggleAction, AdminSidebarToggleAction } from './admin-sidebar.actions';
import { AppState, keySelector } from '../../app.reducer';
import { slideSidebar } from '../../shared/animations/slide';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
const sidebarSectionStateSelector = (state: AdminSidebarSectionsState) => state.adminSidebarSection; const sidebarSectionStateSelector = (state: AppState) => state.adminSidebar.sections;
const sidebarStateSelector = (state) => state.adminSidebar;
const sectionByNameSelector = (name: string): MemoizedSelector<AdminSidebarSectionsState, AdminSidebarSectionState> => { const sectionByNameSelector = (name: string): MemoizedSelector<AppState, AdminSidebarSectionState> => {
return keySelector<AdminSidebarSectionState>(name); return keySelector<AdminSidebarSectionState>(name, sidebarSectionStateSelector);
}; };
export function keySelector<T>(key: string): MemoizedSelector<AdminSidebarSectionsState, T> {
return createSelector(sidebarSectionStateSelector, (state: AdminSidebarSectionState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}
@Component({ @Component({
selector: 'ds-admin-sidebar', selector: 'ds-admin-sidebar',
templateUrl: './admin-sidebar.component.html', templateUrl: './admin-sidebar.component.html',
styleUrls: ['./admin-sidebar.component.scss'], styleUrls: ['./admin-sidebar.component.scss'],
animations: [slideSidebar]
}) })
export class AdminSidebarComponent { export class AdminSidebarComponent implements OnInit {
constructor(private store: Store<AdminSidebarSectionsState>) { sidebarCollapsed: Observable<boolean>;
sidebarWidth: Observable<string>;
constructor(private store: Store<AdminSidebarState>,
private variableService: CSSVariableService) {
}
ngOnInit(): void {
this.sidebarWidth = this.variableService.getVariable('adminSidebarWidth')
this.sidebarCollapsed = this.store.pipe(
select(sidebarStateSelector),
map((state: AdminSidebarState) => state.collapsed)
);
} }
public active(name: string): Observable<boolean> { public active(name: string): Observable<boolean> {
@@ -38,8 +45,13 @@ export class AdminSidebarComponent {
); );
} }
toggle(event: Event, name: string) { toggleSection(event: Event, name: string) {
event.preventDefault(); event.preventDefault();
this.store.dispatch(new AdminSidebarSectionToggleAction(name)); this.store.dispatch(new AdminSidebarSectionToggleAction(name));
} }
toggle(event: Event) {
event.preventDefault();
this.store.dispatch(new AdminSidebarToggleAction());
}
} }

View File

@@ -1,56 +1,100 @@
import { AdminSidebarSectionAction, AdminSidebarSectionActionTypes } from './admin-sidebar.actions'; import {
AdminSidebarAction,
AdminSidebarSectionAction,
AdminSidebarActionTypes
} from './admin-sidebar.actions';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
/** /**
* Interface that represents the state for a single section * Interface that represents the state for a single sidebar section
*/ */
export interface AdminSidebarSectionState { export interface AdminSidebarSectionState {
sectionCollapsed: boolean, sectionCollapsed: boolean;
} }
/**
* Interface that represents the state for all available sections
*/
export interface AdminSidebarSectionsState { export interface AdminSidebarSectionsState {
[name: string]: AdminSidebarSectionState [name: string]: AdminSidebarSectionState;
} }
const initialState: AdminSidebarSectionsState = Object.create(null);
const initiallyCollapsed = true;
/** /**
* Performs a search section action on the current state * Interface that represents the state of the admin sidebar and its sections
* @param {AdminSidebarSectionsState} state The state before the action is performed
* @param {AdminSidebarSectionAction} action The action that should be performed
* @returns {AdminSidebarSectionsState} The state after the action is performed
*/ */
export function sidebarSectionReducer(state = initialState, action: AdminSidebarSectionAction): AdminSidebarSectionsState { export interface AdminSidebarState {
sections: AdminSidebarSectionsState,
collapsed: boolean;
}
const initialState: AdminSidebarState = Object.create(null);
const initiallySectionCollapsed = true;
const initiallyCollapsed = false;
/**
* Performs a sidebar action on the current state
* @param {AdminSidebarState} state The state before the action is performed
* @param {AdminSidebarAction} action The action that should be performed
* @returns {AdminSidebarState} The state after the action is performed
*/
export function adminSidebarReducer(state = initialState, action: AdminSidebarAction): AdminSidebarState {
if (action instanceof AdminSidebarSectionAction) {
return reduceSectionAction(state, action);
} else {
switch (action.type) { switch (action.type) {
case AdminSidebarSectionActionTypes.COLLAPSE: { case AdminSidebarActionTypes.COLLAPSE: {
return Object.assign({}, state, { return Object.assign({}, state, {
collapsed: true
});
}
case AdminSidebarActionTypes.EXPAND: {
return Object.assign({}, state, {
collapsed: false
});
}
case AdminSidebarActionTypes.TOGGLE: {
const currentState = state.collapsed;
const collapsed = hasValue(currentState) ? currentState : initiallyCollapsed;
return Object.assign({}, state, {
collapsed: !collapsed
});
}
default: {
return state;
}
}
}
}
function reduceSectionAction(state: AdminSidebarState, action: AdminSidebarSectionAction): AdminSidebarState {
switch (action.type) {
case AdminSidebarActionTypes.SECTION_COLLAPSE: {
const sections = Object.assign({}, state.sections, {
[action.sectionName]: { [action.sectionName]: {
sectionCollapsed: true, sectionCollapsed: true,
} }
}); });
return Object.assign({}, state, { sections });
} }
case AdminSidebarSectionActionTypes.EXPAND: { case AdminSidebarActionTypes.SECTION_EXPAND: {
return Object.assign({}, state, { const sections = Object.assign({}, state.sections, {
[action.sectionName]: { [action.sectionName]: {
sectionCollapsed: false, sectionCollapsed: false,
} }
}); });
return Object.assign({}, state, { sections });
} }
case AdminSidebarSectionActionTypes.TOGGLE: { case AdminSidebarActionTypes.SECTION_TOGGLE: {
const currentState = state[action.sectionName]; const currentState = state.sections;
const collapsed = hasValue(currentState) ? currentState.sectionCollapsed : initiallyCollapsed; const collapsed = (hasValue(currentState) && currentState[action.sectionName]) ? currentState[action.sectionName].sectionCollapsed : initiallySectionCollapsed;
return Object.assign({}, state, { const sections = Object.assign({}, state.sections, {
[action.sectionName]: { [action.sectionName]: {
sectionCollapsed: !collapsed, sectionCollapsed: !collapsed,
} }
}); });
return Object.assign({}, state, { sections });
} }
default: { default: {

View File

@@ -8,6 +8,9 @@
.dspace-logo-container { .dspace-logo-container {
margin: 10px 20px 0px 20px; margin: 10px 20px 0px 20px;
.display-3 {
word-break: break-word;
}
} }
.dspace-logo-container img { .dspace-logo-container img {

View File

@@ -1,5 +1,5 @@
@import '../styles/variables.scss'; @import '../styles/variables.scss';
@import '../styles/font-awesome-imports.scss'; @import '../styles/helpers/font_awesome_imports.scss';
@import '../../node_modules/bootstrap/scss/bootstrap.scss'; @import '../../node_modules/bootstrap/scss/bootstrap.scss';
@import '../../node_modules/nouislider/distribute/nouislider.min.css'; @import '../../node_modules/nouislider/distribute/nouislider.min.css';

View File

@@ -23,6 +23,8 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
import { isAuthenticated } from './core/auth/selectors'; import { isAuthenticated } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service'; import { AuthService } from './core/auth/auth.service';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
@Component({ @Component({
selector: 'ds-app', selector: 'ds-app',
@@ -42,7 +44,8 @@ export class AppComponent implements OnInit, AfterViewInit {
private metadata: MetadataService, private metadata: MetadataService,
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
private authService: AuthService, private authService: AuthService,
private router: Router private router: Router,
private cssService: CSSVariableService
) { ) {
// this language will be used as a fallback when a translation isn't found in the current language // this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en'); translate.setDefaultLang('en');
@@ -54,6 +57,8 @@ export class AppComponent implements OnInit, AfterViewInit {
if (config.debug) { if (config.debug) {
console.info(config); console.info(config);
} }
this.storeCSSVariables();
} }
ngOnInit() { ngOnInit() {
@@ -67,7 +72,13 @@ export class AppComponent implements OnInit, AfterViewInit {
first(), first(),
filter((authenticated) => !authenticated) filter((authenticated) => !authenticated)
).subscribe((authenticated) => this.authService.checkAuthenticationToken()); ).subscribe((authenticated) => this.authService.checkAuthenticationToken());
}
private storeCSSVariables() {
const vars = variables.locals;
Object.keys(vars).forEach((name: string) => {
this.cssService.addCSSVariable(name, vars[name]);
})
} }
ngAfterViewInit() { ngAfterViewInit() {

View File

@@ -1,4 +1,4 @@
import { ActionReducerMap } from '@ngrx/store'; import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer'; import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer'; import { formReducer, FormState } from './shared/form/form.reducer';
@@ -17,9 +17,11 @@ import {
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { navbarReducer, NavbarState } from './navbar/navbar.reducer'; import { navbarReducer, NavbarState } from './navbar/navbar.reducer';
import { import {
AdminSidebarSectionsState, adminSidebarReducer,
sidebarSectionReducer AdminSidebarState
} from './+admin/admin-sidebar/admin-sidebar.reducer'; } from './+admin/admin-sidebar/admin-sidebar.reducer';
import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
@@ -30,7 +32,8 @@ export interface AppState {
searchSidebar: SearchSidebarState; searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState; searchFilter: SearchFiltersState;
truncatable: TruncatablesState; truncatable: TruncatablesState;
adminSidebarSection: AdminSidebarSectionsState; adminSidebar: AdminSidebarState;
cssVariables: CSSVariablesState;
} }
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
@@ -42,7 +45,18 @@ export const appReducers: ActionReducerMap<AppState> = {
searchSidebar: sidebarReducer, searchSidebar: sidebarReducer,
searchFilter: filterReducer, searchFilter: filterReducer,
truncatable: truncatableReducer, truncatable: truncatableReducer,
adminSidebarSection: sidebarSectionReducer adminSidebar: adminSidebarReducer,
cssVariables: cssVariablesReducer,
}; };
export const routerStateSelector = (state: AppState) => state.router; export const routerStateSelector = (state: AppState) => state.router;
export function keySelector<T>(key: string, selector): MemoizedSelector<AppState, T> {
return createSelector(selector, (state) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -64,6 +64,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -128,6 +129,7 @@ const PROVIDERS = [
UploaderService, UploaderService,
UUIDService, UUIDService,
DSpaceObjectDataService, DSpaceObjectDataService,
CSSVariableService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -17,7 +17,7 @@ export class NavbarComponent {
constructor( constructor(
private store: Store<AppState>, private store: Store<AppState>,
protected windowService: HostWindowService public windowService: HostWindowService
) { ) {
} }

View File

@@ -2,18 +2,18 @@ import { animate, state, transition, trigger, style } from '@angular/animations'
export const focusShadow = trigger('focusShadow', [ export const focusShadow = trigger('focusShadow', [
state('focus', style({ 'box-shadow': 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })), state('focus', style({ boxShadow: 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })),
state('blur', style({ 'box-shadow': 'none' })), state('blur', style({ boxShadow: 'none' })),
transition('focus <=> blur', animate(250)) transition('focus <=> blur', [animate('250ms')])
]); ]);
export const focusBackground = trigger('focusBackground', [ export const focusBackground = trigger('focusBackground', [
state('focus', style({ 'background-color': 'rgba(119, 119, 119, 0.1)' })), state('focus', style({ backgroundColor: 'rgba(119, 119, 119, 0.1)' })),
state('blur', style({ 'background-color': 'transparent' })), state('blur', style({ backgroundColor: 'transparent' })),
transition('focus <=> blur', animate(250)) transition('focus <=> blur', [animate('250ms')])
]); ]);

View File

@@ -6,7 +6,7 @@ export const slide = trigger('slide', [
state('collapsed', style({ height: 0 })), state('collapsed', style({ height: 0 })),
transition('expanded <=> collapsed', animate(250)) transition('expanded <=> collapsed', animate('250ms'))
]); ]);
export const slideMobileNav = trigger('slideMobileNav', [ export const slideMobileNav = trigger('slideMobileNav', [
@@ -15,5 +15,17 @@ export const slideMobileNav = trigger('slideMobileNav', [
state('collapsed', style({ height: 0 })), state('collapsed', style({ height: 0 })),
transition('expanded <=> collapsed', animate(300)) transition('expanded <=> collapsed', animate('300ms'))
]);
export const slideSidebar = trigger('slideSidebar', [
state('expanded',
style({ width: '{{ sidebarWidth }}' }),
{ params: { sidebarWidth: '*' } }
),
state('collapsed', style({ width: '*' })),
transition('expanded <=> collapsed', animate('300ms ease-in-out')),
]); ]);

View File

@@ -3,7 +3,13 @@ import { cold, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { GridBreakpoint, HostWindowService, WidthCategory } from './host-window.service'; import { HostWindowService, WidthCategory } from './host-window.service';
enum GridBreakpoint {
SM_MIN = 576,
MD_MIN = 768,
LG_MIN = 992,
XL_MIN = 1200
}
describe('HostWindowService', () => { describe('HostWindowService', () => {
let service: HostWindowService; let service: HostWindowService;

View File

@@ -1,20 +1,13 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { filter, distinctUntilChanged, map } from 'rxjs/operators'; import { filter, distinctUntilChanged, map, first } from 'rxjs/operators';
import { HostWindowState } from './host-window.reducer'; import { HostWindowState } from './host-window.reducer';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { hasValue } from './empty.util'; import { hasValue } from './empty.util';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { CSSVariableService } from './sass-helper/sass-helper.service';
// TODO: ideally we should get these from sass somehow
export enum GridBreakpoint {
SM_MIN = 576,
MD_MIN = 768,
LG_MIN = 992,
XL_MIN = 1200
}
export enum WidthCategory { export enum WidthCategory {
XS, XS,
@@ -29,10 +22,19 @@ const widthSelector = createSelector(hostWindowStateSelector, (hostWindow: HostW
@Injectable() @Injectable()
export class HostWindowService { export class HostWindowService {
private breakPoints: { XS_MIN, SM_MIN, MD_MIN, LG_MIN, XL_MIN } = {} as any;
constructor( constructor(
private store: Store<AppState> private store: Store<AppState>,
private variableService: CSSVariableService
) { ) {
/* See _exposed_variables.scss */
variableService.getAllVariables().pipe(first()).subscribe((variables) => {
this.breakPoints.XL_MIN = parseInt(variables.xlMin, 10);
this.breakPoints.LG_MIN = parseInt(variables.lgMin, 10);
this.breakPoints.MD_MIN = parseInt(variables.mdMin, 10);
this.breakPoints.SM_MIN = parseInt(variables.smMin, 10);
});
} }
private getWidthObs(): Observable<number> { private getWidthObs(): Observable<number> {
@@ -45,13 +47,13 @@ export class HostWindowService {
get widthCategory(): Observable<WidthCategory> { get widthCategory(): Observable<WidthCategory> {
return this.getWidthObs().pipe( return this.getWidthObs().pipe(
map((width: number) => { map((width: number) => {
if (width < GridBreakpoint.SM_MIN) { if (width < this.breakPoints.SM_MIN) {
return WidthCategory.XS return WidthCategory.XS
} else if (width >= GridBreakpoint.SM_MIN && width < GridBreakpoint.MD_MIN) { } else if (width >= this.breakPoints.SM_MIN && width < this.breakPoints.MD_MIN) {
return WidthCategory.SM return WidthCategory.SM
} else if (width >= GridBreakpoint.MD_MIN && width < GridBreakpoint.LG_MIN) { } else if (width >= this.breakPoints.MD_MIN && width < this.breakPoints.LG_MIN) {
return WidthCategory.MD return WidthCategory.MD
} else if (width >= GridBreakpoint.LG_MIN && width < GridBreakpoint.XL_MIN) { } else if (width >= this.breakPoints.LG_MIN && width < this.breakPoints.XL_MIN) {
return WidthCategory.LG return WidthCategory.LG
} else { } else {
return WidthCategory.XL return WidthCategory.XL

View File

@@ -0,0 +1,29 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/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 CSSVariableActionTypes = {
ADD: type('dspace/css-variables/ADD'),
};
export class AddCSSVariableAction implements Action {
type = CSSVariableActionTypes.ADD;
payload: {
name: string,
value: string
};
constructor(name: string, value: string) {
this.payload = {name, value};
}
}
/* tslint:enable:max-classes-per-file */
export type CSSVariableAction = AddCSSVariableAction

View File

@@ -0,0 +1,20 @@
import { CSSVariableAction, CSSVariableActionTypes } from './sass-helper.actions';
export interface CSSVariablesState {
[name: string]: string;
}
const initialState: CSSVariablesState = Object.create({});
export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState {
switch (action.type) {
case CSSVariableActionTypes.ADD: {
const variable = action.payload;
const t = Object.assign({}, state, { [variable.name]: variable.value });
return t;
}
default: {
return state;
}
}
}

View File

@@ -0,0 +1,33 @@
import { Inject, Injectable } from '@angular/core';
import { AppState, keySelector } from '../../app.reducer';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { AddCSSVariableAction } from './sass-helper.actions';
@Injectable()
export class CSSVariableService {
constructor(
protected store: Store<AppState>,
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
}
addCSSVariable(name: string, value: string) {
this.store.dispatch(new AddCSSVariableAction(name, value));
}
getVariable(name: string) {
return this.store.pipe(select(themeVariableByNameSelector(name)));
}
getAllVariables() {
return this.store.pipe(select(themeVariablesSelector));
}
}
const themeVariablesSelector = (state: AppState) => state.cssVariables;
const themeVariableByNameSelector = (name: string): MemoizedSelector<AppState, string> => {
return keySelector<string>(name, themeVariablesSelector);
};

View File

@@ -82,3 +82,8 @@ declare module '*.json' {
} }
declare module 'reflect-metadata'; declare module 'reflect-metadata';
declare module '*.scss' {
const content: any;
export default content;
}

View File

@@ -0,0 +1,9 @@
@import '_variables.scss';
:export {
adminSidebarWidth: $admin-sidebar-width;
xlMin: map-get($grid-breakpoints, xl);
mdMin: map-get($grid-breakpoints, md);
lgMin: map-get($grid-breakpoints, lg);
smMin: map-get($grid-breakpoints, sm);
}

View File

@@ -1,5 +1,5 @@
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
const { const {
root, root,
join join
@@ -37,7 +37,8 @@ module.exports = {
{ {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
sourceMap: true sourceMap: true,
modules: true
} }
}, },
{ {
@@ -50,7 +51,9 @@ module.exports = {
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: [/node_modules/,
path.resolve(__dirname, '..', 'src/styles/_exposed_variables.scss')
],
use: [{ use: [{
loader: 'to-string-loader', loader: 'to-string-loader',
options: { options: {
@@ -61,12 +64,6 @@ module.exports = {
options: { options: {
sourceMap: true sourceMap: true
} }
},
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
}, },
{ {
loader: 'resolve-url-loader', loader: 'resolve-url-loader',
@@ -82,6 +79,15 @@ module.exports = {
} }
] ]
}, },
{
test: /_exposed_variables.scss$/,
exclude: /node_modules/,
use: [{
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "sass-loader" // compiles Sass to CSS
}]
},
{ {
test: /\.html$/, test: /\.html$/,
loader: 'raw-loader' loader: 'raw-loader'