refractor: shell (#287)

* Refractor components.
* Shift theme picker component to shared module.
* Shift progress bar service into its own folder.
* Add footer to app shell.
* Replace mat-icon by font-awesome icons.
* Fix sidenav visibility bug for handsets.
* Add comments.

Closes #286, Fixes #285
This commit is contained in:
Abhay Chawla
2018-08-11 00:44:50 +05:30
committed by GitHub
parent 2330f70f2c
commit 1526235e2b
39 changed files with 613 additions and 379 deletions
@@ -14,7 +14,7 @@
font-weight: 400;
}
.separator:before {
.separator::before {
content: "";
border: 1.5px solid #000;
margin: 0 0.6rem;
@@ -30,19 +30,17 @@
a {
text-decoration: none;
color: #0275d8;
&:hover {
text-decoration: underline;
}
}
a:hover {
text-decoration: underline;
&::after {
content: "/";
margin: 0 0.4rem;
}
}
.breadcrumb-link:after {
content: "/";
margin: 0 0.4rem;
}
}
}
}
@@ -1,10 +1,45 @@
/** Angular Imports */
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
/** rxjs Imports */
import { filter } from 'rxjs/operators';
/** Custom Model */
import { Breadcrumb } from './breadcrumb.model';
/**
* Route data property to generate breadcrumb using a static string.
*
* Example- breadcrumb: 'Home'
*/
const routeDataBreadcrumb = 'breadcrumb';
/**
* Route data property to generate breadcrumb using given route parameter name.
*
* Example- routeParamBreadcrumb: 'id'
*/
const routeParamBreadcrumb = 'routeParamBreadcrumb';
/**
* Route data property to generate breadcrumb using resolved data property name.
*
* Use array to specify name for a nested object property.
*
* Example- routeResolveBreadcrumb: ['user', 'username']
*/
const routeResolveBreadcrumb = 'routeResolveBreadcrumb';
/**
* Route data property to specify whether generated breadcrumb should have a link.
*
* True by default. Specify false if a link is not required.
*
* Example- addBreadcrumbLink: false
*/
const routeAddBreadcrumbLink = 'addBreadcrumbLink';
/**
* Generate breadcrumbs dynamically via route configuration.
*/
@Component({
selector: 'mifosx-breadcrumb',
templateUrl: './breadcrumb.component.html',
@@ -12,19 +47,26 @@ import { Breadcrumb } from './breadcrumb.model';
})
export class BreadcrumbComponent implements OnInit {
/** Array of breadcrumbs. */
breadcrumbs: Breadcrumb[];
/**
* Generates the breadcrumbs.
* @param {ActivatedRoute} activatedRoute Activate Route.
* @param {Router} router Router for navigation.
*/
constructor(private activatedRoute: ActivatedRoute,
private router: Router) {
this.generateBreadcrumbs();
}
generateBreadcrumbs() {
const routeDataBreadcrumb = 'breadcrumb';
const routeParamBreadcrumb = 'routeParamBreadcrumb';
const routeResolveBreadcrumb = 'routeResolveBreadcrumb';
const routeAddBreadcrumbLink = 'addBreadcrumbLink';
ngOnInit() {
}
/**
* Generates the array of breadcrumbs for the visited route.
*/
generateBreadcrumbs() {
const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd));
onNavigationEnd.subscribe(() => {
@@ -86,7 +128,4 @@ export class BreadcrumbComponent implements OnInit {
});
}
ngOnInit() {
}
}
@@ -1,3 +1,6 @@
/**
* Breadcrumb model.
*/
export interface Breadcrumb {
label: string;
url: string;
@@ -1 +1,2 @@
<!-- Display the content for the visited route. -->
<router-outlet></router-outlet>
@@ -1,5 +1,9 @@
/** Angular Imports */
import { Component, OnInit } from '@angular/core';
/**
* Content component.
*/
@Component({
selector: 'mifosx-content',
templateUrl: './content.component.html',
+9
View File
@@ -1,5 +1,7 @@
<!-- Application Shell -->
<mat-sidenav-container id="mifosx-shell-container" autosize>
<!-- Sidenav -->
<mat-sidenav
#sidenav
class="sidebar-panel"
@@ -15,13 +17,20 @@
</mat-sidenav>
<!-- Content -->
<mat-sidenav-content>
<!-- Toolbar -->
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
<!-- Progress Bar -->
<mat-progress-bar [mode]="progressBarMode"></mat-progress-bar>
<!-- Breadcrumb -->
<mifosx-breadcrumb></mifosx-breadcrumb>
<!-- Content -->
<mifosx-content></mifosx-content>
<!-- Footer -->
<mifosx-footer></mifosx-footer>
</mat-sidenav-content>
+2 -3
View File
@@ -3,10 +3,10 @@
height: 100vh;
.sidebar-panel {
background-color: white; // TODO: Theme Configuration
background-color: #fff;
min-height: 100vh;
overflow: hidden;
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.sidebar-full {
@@ -16,5 +16,4 @@
.sidebar-compact {
width: 4rem;
}
}
+1 -13
View File
@@ -1,10 +1,5 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AuthenticationService } from '../authentication/authentication.service';
import { MockAuthenticationService } from '../authentication/authentication.service.mock';
import { CoreModule } from '../core.module';
import { ShellComponent } from './shell.component';
describe('ShellComponent', () => {
@@ -13,14 +8,7 @@ describe('ShellComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot(),
CoreModule
],
providers: [
{ provide: AuthenticationService, useClass: MockAuthenticationService }
]
declarations: [ ShellComponent ]
})
.compileComponents();
}));
+33 -8
View File
@@ -1,10 +1,17 @@
/** Angular Imports */
import { Component, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
/** rxjs Imports */
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { ProgressBarService } from '../progress-bar.service';
/** Custom Services */
import { ProgressBarService } from '../progress-bar/progress-bar.service';
/**
* Shell component.
*/
@Component({
selector: 'mifosx-shell',
templateUrl: './shell.component.html',
@@ -12,30 +19,48 @@ import { ProgressBarService } from '../progress-bar.service';
})
export class ShellComponent implements OnInit, OnDestroy {
/** Subscription to breakpoint observer for handset. */
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches)
);
sidenavCollapsed = false;
.pipe(
map(result => result.matches)
);
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
/** Progress bar mode. */
progressBarMode: string;
progressBar$: any;
/** Subscription to progress bar. */
progressBar$: Subscription;
/**
* @param {BreakpointObserver} breakpointObserver Breakpoint Observer to detect screen size.
* @param {ProgressBarService} progressBarService Progress Bar Service.
* @param {ChangeDetectorRef} cdr Change Detector Ref.
*/
constructor(private breakpointObserver: BreakpointObserver,
private progressBarService: ProgressBarService,
private cdr: ChangeDetectorRef) { }
/**
* Subscribes to progress bar to update its mode.
*/
ngOnInit() {
this.sidenavCollapsed = true;
this.progressBar$ = this.progressBarService.updateProgressBar.subscribe((mode: string) => {
this.progressBarMode = mode;
this.cdr.detectChanges();
});
}
/**
* Toggles the current collapsed state of sidenav according to the emitted event.
* @param {boolean} event denotes state of sidenav
*/
toggleCollapse($event: boolean) {
this.sidenavCollapsed = $event;
}
/**
* Unsubscribes from progress bar.
*/
ngOnDestroy() {
this.progressBar$.unsubscribe();
}
@@ -6,6 +6,7 @@
<div class="navigation-panel" fxLayout="column">
<!-- TODO: Allow customization by organization -->
<div class="app-brand">
<img src="assets/images/MifosX_logo.png" alt="app-logo" class="app-logo">
<span class="app-logo-text text-muted">Mifos</span>
@@ -13,41 +14,43 @@
<div class="app-user">
<div class="app-user-photo">
<!-- TODO: Decision to be taken on providing this feature -->
<img src="assets/images/user_placeholder.png" alt="app-user-photo">
</div>
<span class="app-user-name mb-05">
Abhay Chawla
{{ username }}
</span>
<div class="app-user-controls">
<button class="text-muted xs-button" mat-icon-button matTooltip="Settings" routerLink="/settings">
<mat-icon>settings</mat-icon>
<button class="text-muted xs-button" mat-icon-button matTooltip="Settings" [routerLink]="['/settings']">
<fa-icon icon="cog"></fa-icon>
</button>
<button class="text-muted xs-button" mat-icon-button matTooltip="Sign Out" (click)="logout()">
<mat-icon>exit_to_app</mat-icon>
<fa-icon icon="sign-out-alt"></fa-icon>
</button>
</div>
</div>
<div class="app-sidenav">
<!-- TODO: Generate frequently accessed routes dynamically -->
<div class="icon-menu">
<div class="mt-1 mb-1 nav-item-sep">
<mat-divider></mat-divider>
<span class="text-muted icon-menu-title">Frequently Accessed</span>
</div>
<div class="icon-menu-item">
<button mat-raised-button matTooltip="Home">
<mat-icon>home</mat-icon>
<button mat-raised-button matTooltip="Home" [routerLink]="['/home']">
<fa-icon icon="home" size="lg"></fa-icon>
</button>
</div>
<div class="icon-menu-item">
<button mat-raised-button matTooltip="Dashboard">
<mat-icon>dashboard</mat-icon>
<button mat-raised-button matTooltip="Dashboard" [routerLink]="['/dashboard']">
<fa-icon icon="tachometer-alt" size="lg"></fa-icon>
</button>
</div>
<div class="icon-menu-item">
<button mat-raised-button matTooltip="Navigation">
<mat-icon>navigation</mat-icon>
<button mat-raised-button matTooltip="Navigation" [routerLink]="['/navigation']">
<fa-icon icon="location-arrow" size="lg"></fa-icon>
</button>
</div>
</div>
@@ -58,14 +61,36 @@
</div>
<mat-nav-list>
<mat-list-item routerLink="/dashboard">
<mat-icon matListIcon>dashboard</mat-icon>
<mat-list-item [routerLink]="['/dashboard']">
<mat-icon matListIcon>
<fa-icon icon="tachometer-alt" size="sm"></fa-icon>
</mat-icon>
<a matLine>Dashboard</a>
</mat-list-item>
<mat-list-item>
<mat-icon matListIcon>navigation</mat-icon>
<mat-list-item [routerLink]="['/navigation']">
<mat-icon matListIcon>
<fa-icon icon="location-arrow" size="sm"></fa-icon>
</mat-icon>
<a matLine>Navigation</a>
</mat-list-item>
<mat-list-item [routerLink]="['/accounting/frequent-postings']">
<mat-icon matListIcon>
<fa-icon icon="sync" size="sm"></fa-icon>
</mat-icon>
<a matLine>Frequent Postings</a>
</mat-list-item>
<mat-list-item [routerLink]="['/accounting/journal-entries/create']">
<mat-icon matListIcon>
<fa-icon icon="plus" size="sm"></fa-icon>
</mat-icon>
<a matLine>Create Journal Entry</a>
</mat-list-item>
<mat-list-item [routerLink]="['/accounting/chart-of-accounts']">
<mat-icon matListIcon>
<fa-icon icon="sitemap" size="sm"></fa-icon>
</mat-icon>
<a matLine>Chart of Accounts</a>
</mat-list-item>
</mat-nav-list>
</div>
+125 -102
View File
@@ -1,117 +1,140 @@
img {
max-width: 100%;
}
.navigation-panel {
position: absolute;
left: 0;
width: 100%;
height: 100%;
padding-top: 64px;
background: hsla(0,0%,100%,.95);
}
.app-brand {
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
text-align: center;
position: fixed;
width: 100%;
max-width: 15rem;
height: 64px;
line-height: 64px;
top: 0;
left: 0;
z-index: 9999;
color: #444;
.app-logo {
width: auto;
height: 42px;
margin-right: 8px;
}
.app-logo-text {
width: auto;
height: auto;
font-size: 2rem;
font-weight: normal;
}
}
.app-user {
text-align: center;
width: 100%;
padding: 1rem 0 .4rem;
.app-user-photo {
width: 72px;
margin: 0 auto 8px;
height: 72px;
border: 1px solid hsla(210,8%,51%,.13);
padding: 4px;
border-radius: 50%;
.sidebar-full,
.sidebar-compact {
.navigation-panel {
position: absolute;
left: 0;
width: 100%;
height: 100%;
padding-top: 64px;
background: hsla(0, 0%, 100%, 0.95);
}
img {
max-width: 100%;
}
.app-brand {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
position: fixed;
width: 100%;
height: 100%;
border-radius: 50%;
}
max-width: 15rem;
height: 64px;
line-height: 64px;
top: 0;
left: 0;
z-index: 9999;
color: #444;
.app-user-name {
display: block;
font-size: .875rem;
font-weight: 300;
color: rgba(0,0,0,.96);
margin-left: -2px;
}
}
.app-sidenav {
.icon-menu {
padding: 0 24px;
opacity: 1;
.mat-divider {
margin: 0 -24px;
.app-logo {
width: auto;
height: 42px;
margin-right: 8px;
}
.icon-menu-title {
padding-left: 0;
margin-left: -8px;
.app-logo-text {
width: auto;
height: auto;
font-size: 2rem;
font-weight: normal;
}
}
.app-user {
text-align: center;
width: 100%;
padding: 1rem 0 0.4rem;
.app-user-photo {
width: 72px;
margin: 0 auto 8px;
height: 72px;
border: 1px solid hsla(210, 8%, 51%, 0.13);
padding: 4px;
border-radius: 50%;
}
.icon-menu-item {
display: inline-block;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
button {
min-width: auto;
margin: 4px;
.app-user-name {
display: block;
font-size: 0.875rem;
font-weight: 300;
color: rgba(0, 0, 0, 0.96);
margin-left: -2px;
}
.app-user-controls {
.mat-icon-button {
.xs-button {
height: 20px;
width: 20px;
line-height: 20px;
margin: 5px;
}
}
.text-muted {
color: rgba(0, 0, 0, 0.54);
}
}
}
.nav-item-sep {
padding: 0;
margin: 1rem 0;
span {
padding: 16px 0 0 16px;
display: block;
font-size: 12px;
}
}
mat-nav-list {
margin: 0;
padding: 0;
list-style: none;
}
}
.app-sidenav {
.icon-menu {
padding: 0 24px;
opacity: 1;
.nav-item-sep {
span {
padding: 16px 0 0 16px;
display: block;
font-size: 12px;
.mat-divider {
margin: 0 -24px;
}
.icon-menu-title {
padding-left: 0;
margin-left: -8px;
}
.icon-menu-item {
display: inline-block;
button {
min-width: auto;
margin: 4px;
}
}
}
.nav-item-sep {
padding: 0;
margin: 1rem 0;
}
.mat-nav-list {
margin: 0;
padding: 0;
list-style: none;
.mat-list-item {
.mat-list-icon {
height: 30px;
font-size: 22px;
}
}
}
}
}
@@ -131,6 +154,12 @@ img {
}
.app-sidenav {
.nav-item-sep {
span {
display: none;
}
}
.icon-menu {
padding: 8px 0 0;
width: 4rem;
@@ -150,11 +179,5 @@ img {
}
}
}
.nav-item-sep {
span {
display: none;
}
}
}
}
@@ -1,8 +1,13 @@
/** Angular Imports */
import { Component, OnInit, Input } from '@angular/core';
import { Router } from '@angular/router';
/** Custom Services */
import { AuthenticationService } from '../../authentication/authentication.service';
/**
* Sidenav component.
*/
@Component({
selector: 'mifosx-sidenav',
templateUrl: './sidenav.component.html',
@@ -10,14 +15,29 @@ import { AuthenticationService } from '../../authentication/authentication.servi
})
export class SidenavComponent implements OnInit {
/** True if sidenav is in collapsed state. */
@Input() sidenavCollapsed: boolean;
/** Username of authenticated user. */
username: string;
/**
* @param {Router} router Router for navigation.
* @param {AuthenticationService} authenticationService Authentication Service.
*/
constructor(private router: Router,
private authenticationService: AuthenticationService) { }
/**
* Sets the username of the authenticated user.
*/
ngOnInit() {
const credentials = this.authenticationService.getCredentials();
this.username = credentials.username;
}
/**
* Logs out the authenticated user and redirects to login page.
*/
logout() {
this.authenticationService.logout()
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
@@ -1,15 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { ThemeManagerService } from './theme-manager.service';
describe('ThemeManagerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ThemeManagerService]
});
});
it('should be created', inject([ThemeManagerService], (service: ThemeManagerService) => {
expect(service).toBeTruthy();
}));
});
@@ -1,39 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ThemeManagerService {
private themeManagerClass = 'theme-manager';
constructor() { }
setTheme(href: string) {
this.getLinkElement().setAttribute('href', href);
}
removeTheme() {
const existingLinkElement = this.getExistingLinkElement();
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
private getLinkElement() {
return this.getExistingLinkElement() || this.createLinkElement();
}
private getExistingLinkElement() {
return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`);
}
private createLinkElement() {
const linkElement = document.createElement('link');
linkElement.setAttribute('rel', 'stylesheet');
linkElement.classList.add(this.themeManagerClass);
document.head.appendChild(linkElement);
return linkElement;
}
}
@@ -1,17 +0,0 @@
<button mat-icon-button [matMenuTriggerFor]="themeMenu" matTooltip="Color Schemes">
<mat-icon>format_color_fill</mat-icon>
</button>
<mat-menu class="mifosx-theme-picker-menu" #themeMenu="matMenu" x-position="before">
<mat-grid-list cols="2">
<mat-grid-tile *ngFor="let theme of themes">
<div mat-menu-item (click)="installTheme(theme)">
<div class="mifosx-theme-picker-swatch">
<mat-icon class="mifosx-theme-chosen-icon" *ngIf="currentTheme.href === theme.href">check_circle</mat-icon>
<div class="mifosx-theme-picker-primary" [style.background]="theme.primary"></div>
<div class="mifosx-theme-picker-accent" [style.background]="theme.accent"></div>
</div>
</div>
</mat-grid-tile>
</mat-grid-list>
</mat-menu>
@@ -1,57 +0,0 @@
$theme-picker-menu-padding: 8px;
$theme-picker-grid-cell-size: 48px;
$theme-picker-grid-cells-per-row: 2;
$theme-picker-swatch-size: 36px;
$theme-picker-accent-stripe-size: 6px;
.mifosx-theme-picker-menu {
.mat-menu-content {
padding: $theme-picker-menu-padding;
}
[mat-menu-item] {
flex: 0 0 auto;
padding: 0;
overflow: hidden;
}
.mifosx-theme-picker-swatch {
position: relative;
width: $theme-picker-swatch-size;
height: $theme-picker-swatch-size;
margin: ($theme-picker-grid-cell-size - $theme-picker-swatch-size) / 2;
border-radius: 50%;
overflow: hidden;
.mifosx-theme-chosen-icon {
color: white;
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid rgba(0,0,0,.2);
border-radius: 50%;
}
}
.mifosx-theme-picker-primary {
width: 100%;
height: 100%;
}
.mifosx-theme-picker-accent {
position: absolute;
bottom: $theme-picker-accent-stripe-size;
width: 100%;
height: $theme-picker-accent-stripe-size;
}
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ThemePickerComponent } from './theme-picker.component';
describe('ThemePickerComponent', () => {
let component: ThemePickerComponent;
let fixture: ComponentFixture<ThemePickerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ThemePickerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ThemePickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,69 +0,0 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Theme } from './theme.model';
import { ThemeStorageService } from './theme-storage.service';
@Component({
selector: 'mifosx-theme-picker',
templateUrl: './theme-picker.component.html',
styleUrls: ['./theme-picker.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ThemePickerComponent implements OnInit {
currentTheme: Theme = {
href: 'denim-yellowgreen.css',
primary: '#1074B9',
accent: '#B4D575',
isDark: false,
isDefault: true
};
themes = [
this.currentTheme,
{
href: 'pictonblue-yellowgreen.css',
primary: '#1DAEEC',
accent: '#B4D575',
isDark: false
},
{
href: 'indigo-pink.css',
primary: '#3F51B5',
accent: '#E91E63',
isDark: false,
},
{
href: 'deeppurple-amber.css',
primary: '#673AB7',
accent: '#FFC107',
isDark: false
},
{ // TODO: Remove Dark Themes
href: 'pink-bluegrey.css',
primary: '#E91E63',
accent: '#607D8B',
isDark: true
},
{
href: 'purple-green.css',
primary: '#9C27B0',
accent: '#4CAF50',
isDark: true
}
];
constructor(public themeStorageService: ThemeStorageService) { }
ngOnInit() {
const theme = this.themeStorageService.getTheme();
if (theme) {
this.currentTheme = theme;
}
}
installTheme(theme: Theme) {
this.currentTheme = theme;
this.themeStorageService.installTheme(theme);
}
}
@@ -1,15 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { ThemeStorageService } from './theme-storage.service';
describe('ThemeStorageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ThemeStorageService]
});
});
it('should be created', inject([ThemeStorageService], (service: ThemeStorageService) => {
expect(service).toBeTruthy();
}));
});
@@ -1,40 +0,0 @@
import {Injectable, EventEmitter} from '@angular/core';
import { Theme } from './theme.model';
import { ThemeManagerService } from './theme-manager.service';
@Injectable({
providedIn: 'root'
})
export class ThemeStorageService {
private themeStorageKey = 'mifosXTheme';
onThemeUpdate: EventEmitter<Theme>;
constructor(public themeManagerService: ThemeManagerService) {
this.onThemeUpdate = new EventEmitter<Theme>();
}
storeTheme(mifosXTheme: Theme) {
localStorage.setItem(this.themeStorageKey, JSON.stringify(mifosXTheme));
this.onThemeUpdate.emit(mifosXTheme);
}
getTheme(): Theme {
return JSON.parse(localStorage.getItem(this.themeStorageKey));
}
clearTheme() {
localStorage.removeItem(this.themeStorageKey);
}
installTheme(theme: Theme) {
if (theme.isDefault) {
this.themeManagerService.removeTheme();
} else {
this.themeManagerService.setTheme(`theme/${theme.href}`);
}
this.storeTheme(theme);
}
}
@@ -1,7 +0,0 @@
export interface Theme {
href: string;
primary: string;
accent: string;
isDark?: boolean;
isDefault?: boolean;
}
@@ -1,37 +1,37 @@
<mat-toolbar fxLayout="row" color="primary" class="toolbar">
<mat-toolbar fxLayout="row" color="primary" id="mifosx-toolbar">
<button mat-icon-button *ngIf="!sidenavCollapsed" (click)="toggleSidenav()" matTooltip="Toggle Hide/Open">
<mat-icon>menu</mat-icon>
<fa-icon icon="bars" size="lg"></fa-icon>
</button>
<button mat-icon-button *ngIf="sidenav.opened" (click)="toggleCollapse()" matTooltip="Toggle Collapse" fxHide.lt-md="true">
<mat-icon *ngIf="!sidenavCollapsed">chevron_left</mat-icon>
<mat-icon *ngIf="sidenavCollapsed">chevron_right</mat-icon>
<button mat-icon-button *ngIf="sidenav.opened" (click)="toggleSidenavCollapse()" matTooltip="Toggle Collapse" fxHide.lt-md="true">
<fa-icon *ngIf="!sidenavCollapsed" icon="chevron-left"></fa-icon>
<fa-icon *ngIf="sidenavCollapsed" icon="chevron-right"></fa-icon>
</button>
<nav mat-tab-nav-bar backgroundColor="primary" class="ml-1">
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="institutionMenu" #institutionMenuTrigger="matMenuTrigger">
<mat-icon class="mr-05">business</mat-icon>
<fa-icon class="mr-05" icon="university" size="lg"></fa-icon>
Institution
</a>
<span fxHide.lt-lg="true">
<a mat-tab-link class="tab-link" [routerLink]="['/accounting']">
<mat-icon class="mr-05">account_balance</mat-icon>
<fa-icon class="mr-05" icon="money-bill-alt" size="lg"></fa-icon>
Accounting
</a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="reportsMenu" #reportsMenuTrigger="matMenuTrigger" >
<mat-icon class="mr-05">insert_chart_outlined</mat-icon>
<fa-icon class="mr-05" icon="chart-bar" size="lg"></fa-icon>
Reports
</a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="adminMenu" #adminMenuTrigger="matMenuTrigger">
<mat-icon class="mr-05">security</mat-icon>
<fa-icon class="mr-05" icon="shield-alt" size="lg"></fa-icon>
Admin
</a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="selfServiceMenu" #selfServiceMenuTrigger="matMenuTrigger">
<mat-icon class="mr-05">people</mat-icon>
<fa-icon class="mr-05" icon="users" size="lg"></fa-icon>
Self Service
</a>
@@ -41,73 +41,90 @@
<span fxFlex="auto"></span>
<!--
TODO: Search
Separate into its own component in shared module for reuse.
Implement functionality.
-->
<div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" [@fadeInOut]>
<mat-form-field class="search">
<input matInput type="text" placeholder="Search">
<mat-label>Search</mat-label>
<input matInput type="text">
</mat-form-field>
</div>
<button mat-icon-button class="ml-1" (click)="toggleVisibility()" fxHide.lt-sm="true">
<mat-icon>search</mat-icon>
<button mat-icon-button class="ml-1" (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
<fa-icon icon="search" size="lg"></fa-icon>
</button>
<mat-select placeholder="Language" [(value)]="selectedLanguage" class="ml-1 language" fxHide.lt-md>
<mat-option value="English">English</mat-option>
<mat-option value="Hindi">Hindi</mat-option>
</mat-select>
<mifosx-language-selector class="ml-1 language" fxHide.lt-md></mifosx-language-selector>
<mifosx-theme-picker class="ml-1" fxHide.lt-md></mifosx-theme-picker>
<mifosx-theme-picker fxHide.lt-md></mifosx-theme-picker>
<!--
TODO: Notifications
Separate into its own component in shared module for reuse.
Implement functionality.
-->
<button mat-icon-button class="ml-1" matTooltip="Notifications">
<mat-icon>notifications</mat-icon>
<fa-icon icon="bell" size="lg"></fa-icon>
</button>
<button mat-icon-button class="ml-1 img-button">
<button mat-icon-button class="ml-1 img-button" [matMenuTriggerFor]="applicationMenu">
<img src="assets/images/user_placeholder.png">
</button>
</mat-toolbar>
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false">
<span>
<button mat-menu-item routerLink="/clients">Clients</button>
<button mat-menu-item routerLink="/groups">Groups</button>
<button mat-menu-item routerLink="/centers">Centers</button>
<span fxHide.lg>
<button mat-menu-item>Accounting</button>
<button mat-menu-item [matMenuTriggerFor]="reportsMenu">Reports</button>
<button mat-menu-item [matMenuTriggerFor]="adminMenu">Admin</button>
<button mat-menu-item [matMenuTriggerFor]="selfServiceMenu">Self Service</button>
</span>
<button mat-menu-item routerLink="/clients">Clients</button>
<button mat-menu-item routerLink="/groups">Groups</button>
<button mat-menu-item routerLink="/centers">Centers</button>
<span fxHide.lg>
<button mat-menu-item [routerLink]="['/accounting']">Accounting</button>
<button mat-menu-item [matMenuTriggerFor]="reportsMenu">Reports</button>
<button mat-menu-item [matMenuTriggerFor]="adminMenu">Admin</button>
<button mat-menu-item [matMenuTriggerFor]="selfServiceMenu">Self Service</button>
</span>
</mat-menu>
<!-- Toolbar Menus -->
<mat-menu #reportsMenu="matMenu" [overlapTrigger]="false">
<span (mouseleave)="reportsMenuTrigger.closeMenu()">
<button mat-menu-item>All</button>
<button mat-menu-item>Clients</button>
<button mat-menu-item>Loans</button>
<button mat-menu-item>Savings</button>
<button mat-menu-item>Funds</button>
<button mat-menu-item>Accounting</button>
<button mat-menu-item>XBRL</button>
</span>
<button mat-menu-item>All</button>
<button mat-menu-item>Clients</button>
<button mat-menu-item>Loans</button>
<button mat-menu-item>Savings</button>
<button mat-menu-item>Funds</button>
<button mat-menu-item>Accounting</button>
<button mat-menu-item>XBRL</button>
</mat-menu>
<mat-menu #adminMenu="matMenu" [overlapTrigger]="false">
<span (mouseleave)="adminMenuTrigger.closeMenu()">
<button mat-menu-item>Users</button>
<button mat-menu-item>Organisation</button>
<button mat-menu-item>System</button>
<button mat-menu-item>Products</button>
<button mat-menu-item>Templates</button>
</span>
<button mat-menu-item>Users</button>
<button mat-menu-item>Organisation</button>
<button mat-menu-item>System</button>
<button mat-menu-item>Products</button>
<button mat-menu-item>Templates</button>
</mat-menu>
<mat-menu #selfServiceMenu="matMenu" [overlapTrigger]="false">
<span (mouseleave)="selfServiceMenuTrigger.closeMenu()">
<button mat-menu-item routerLink="/self-service/users">User Management</button>
<button mat-menu-item routerLink="/self-service/app-configuration">App Configuration</button>
<button mat-menu-item>Task Management</button>
</span>
<button mat-menu-item [routerLink]="['/self-service/users']">User Management</button>
<button mat-menu-item [routerLink]="['/self-service/app-configuration']">App Configuration</button>
<button mat-menu-item>Task Management</button>
</mat-menu>
<!-- Application User Help Menu -->
<mat-menu #applicationMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item [routerLink]="['/settings']">
<mat-icon>
<fa-icon icon="cog" size="sm"></fa-icon>
</mat-icon>
<span>Settings</span>
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>
<fa-icon icon="sign-out-alt" size="sm"></fa-icon>
</mat-icon>
<span>Sign Out</span>
</button>
</mat-menu>
@@ -1,45 +1,40 @@
mat-form-field {
font-size: 1rem;
margin-top: 6px;
}
.toolbar {
#mifosx-toolbar {
position: relative;
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
}
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
.tab-link {
min-width: auto;
padding: 0 16px;
}
.tab-link {
min-width: auto;
padding: 0 1rem;
}
.search-bar {
mat-form-field {
.search-bar {
.search {
max-width: 400px;
width: 100%;
font-size: 1rem;
}
}
.language {
min-width: 80px;
width: 80px;
font-size: 0.875rem;
margin-top: 0.375rem;
}
.img-button {
img {
width: 32px;
height: 32px;
border-radius: 50%;
}
}
.ml-1 {
margin-left: 1rem;
}
.mr-1 {
margin-right: 1rem;
max-width: 400px;
width: 100%;
margin-left: 20px;
}
}
.language {
min-width: 80px;
width: 80px;
font-size: .875rem;
}
.ml-1 {
margin-left: 1rem;
}
.mr-1 {
margin-right: 1rem;
}
.img-button {
img {
width: 32px;
height: 32px;
border-radius: 50%;
}
}
+53 -19
View File
@@ -1,11 +1,20 @@
/** Angular Imports */
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { MatSidenav, MatMenuTrigger } from '@angular/material';
import { MatSidenav } from '@angular/material';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import {style, state, animate, transition, trigger} from '@angular/animations';
import { style, animate, transition, trigger } from '@angular/animations';
import { Router } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
/** Custom Services */
import { AuthenticationService } from '../../authentication/authentication.service';
/**
* Toolbar component.
*/
@Component({
selector: 'mifosx-toolbar',
templateUrl: './toolbar.component.html',
@@ -24,45 +33,70 @@ import { map } from 'rxjs/operators';
})
export class ToolbarComponent implements OnInit {
/** Subscription to breakpoint observer for handset. */
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(
map(result => result.matches)
);
.pipe(
map(result => result.matches)
);
isHandset: boolean;
/** Sets the initial visibility of search input as hidden. Visible if true. */
searchVisible = false;
selectedLanguage = 'English';
sidenavCollapsed = false;
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
/** Instance of sidenav. */
@Input() sidenav: MatSidenav;
/** Sidenav collapse event. */
@Output() collapse = new EventEmitter<boolean>();
constructor(private breakpointObserver: BreakpointObserver) { }
/**
* @param {BreakpointObserver} breakpointObserver Breakpoint observer to detect screen size.
* @param {Router} router Router for navigation.
* @param {AuthenticationService} authenticationService Authentication service.
*/
constructor(private breakpointObserver: BreakpointObserver,
private router: Router,
private authenticationService: AuthenticationService) { }
/**
* Subscribes to breakpoint for handset.
*/
ngOnInit() {
this.sidenavCollapsed = !this.sidenavCollapsed;
this.isHandset$.subscribe(isHandset => {
this.isHandset = isHandset;
if (isHandset && this.sidenavCollapsed) {
this.toggleSidenavCollapse(false);
}
});
}
/**
* Toggles the current state of sidenav.
*/
toggleSidenav() {
this.sidenav.toggle();
}
toggleCollapse() {
this.sidenavCollapsed = !this.sidenavCollapsed;
/**
* Toggles the current collapsed state of sidenav.
*/
toggleSidenavCollapse(sidenavCollapsed?: boolean) {
this.sidenavCollapsed = sidenavCollapsed || !this.sidenavCollapsed;
this.collapse.emit(this.sidenavCollapsed);
}
toggleVisibility() {
/**
* Toggles the visibility of search input with fadeInOut animation.
*/
toggleSearchVisibility() {
this.searchVisible = !this.searchVisible;
}
/* onMouseEnter(menuTrigger: MatMenuTrigger) {
if (!this.isHandset) {
menuTrigger.openMenu();
}
/**
* Logs out the authenticated user and redirects to login page.
*/
logout() {
this.authenticationService.logout()
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
}
*/
}