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
+1 -3
View File
@@ -8,12 +8,11 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { NgxChartsModule } from '@swimlane/ngx-charts'; import { NgxChartsModule } from '@swimlane/ngx-charts';
import { environment } from '../environments/environment'; import { environment } from 'environments/environment';
import { WebAppComponent } from './web-app.component'; import { WebAppComponent } from './web-app.component';
import { CoreModule } from './core/core.module'; import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { NavigationModule } from './navigation/navigation.module'; import { NavigationModule } from './navigation/navigation.module';
import { HomeModule } from './home/home.module'; import { HomeModule } from './home/home.module';
import { AboutModule } from './about/about.module'; import { AboutModule } from './about/about.module';
@@ -37,7 +36,6 @@ import { SettingsModule } from './settings/settings.module';
TranslateModule.forRoot(), TranslateModule.forRoot(),
NgxChartsModule, NgxChartsModule,
CoreModule, CoreModule,
SharedModule,
HomeModule, HomeModule,
NavigationModule, NavigationModule,
AboutModule, AboutModule,
+6 -47
View File
@@ -1,5 +1,4 @@
import { NgModule, Optional, SkipSelf } from '@angular/core'; import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
import { RouteReuseStrategy, RouterModule } from '@angular/router'; import { RouteReuseStrategy, RouterModule } from '@angular/router';
@@ -46,70 +45,32 @@ import { TranslateModule } from '@ngx-translate/core';
import { RouteReusableStrategy } from './route-reusable-strategy'; import { RouteReusableStrategy } from './route-reusable-strategy';
import { AuthenticationService } from './authentication/authentication.service'; import { AuthenticationService } from './authentication/authentication.service';
import { AuthenticationGuard } from './authentication/authentication.guard'; import { AuthenticationGuard } from './authentication/authentication.guard';
import { I18nService } from './i18n.service';
import { HttpService } from './http/http.service'; import { HttpService } from './http/http.service';
import { HttpCacheService } from './http/http-cache.service'; import { HttpCacheService } from './http/http-cache.service';
import { ProgressBarService } from './progress-bar.service'; import { ProgressBarService } from './progress-bar/progress-bar.service';
import { ProgressInterceptor } from './progress.interceptor'; import { ProgressInterceptor } from './progress-bar/progress.interceptor';
import { ApiPrefixInterceptor } from './http/api-prefix.interceptor'; import { ApiPrefixInterceptor } from './http/api-prefix.interceptor';
import { ErrorHandlerInterceptor } from './http/error-handler.interceptor'; import { ErrorHandlerInterceptor } from './http/error-handler.interceptor';
import { CacheInterceptor } from './http/cache.interceptor'; import { CacheInterceptor } from './http/cache.interceptor';
import { ShellComponent } from './shell/shell.component'; import { ShellComponent } from './shell/shell.component';
import { SidenavComponent } from './shell/sidenav/sidenav.component'; import { SidenavComponent } from './shell/sidenav/sidenav.component';
import { ThemePickerComponent } from './shell/toolbar/theme-picker/theme-picker.component';
import { ToolbarComponent } from './shell/toolbar/toolbar.component'; import { ToolbarComponent } from './shell/toolbar/toolbar.component';
import { ContentComponent } from './shell/content/content.component'; import { ContentComponent } from './shell/content/content.component';
import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component'; import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
import { AuthenticationInterceptor } from './authentication/authentication.interceptor'; import { AuthenticationInterceptor } from './authentication/authentication.interceptor';
import { SharedModule } from '../shared';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, SharedModule,
HttpClientModule, HttpClientModule,
TranslateModule, TranslateModule,
RouterModule, RouterModule
LayoutModule,
FlexLayoutModule,
MatCheckboxModule,
MatCheckboxModule,
MatButtonModule,
MatInputModule,
MatAutocompleteModule,
MatDatepickerModule,
MatNativeDateModule,
MatFormFieldModule,
MatRadioModule,
MatSelectModule,
MatSliderModule,
MatSlideToggleModule,
MatMenuModule,
MatSidenavModule,
MatToolbarModule,
MatListModule,
MatGridListModule,
MatCardModule,
MatStepperModule,
MatTabsModule,
MatExpansionModule,
MatButtonToggleModule,
MatChipsModule,
MatIconModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatDialogModule,
MatTooltipModule,
MatSnackBarModule,
MatTableModule,
MatTreeModule,
MatSortModule,
MatPaginatorModule,
MatDividerModule
], ],
declarations: [ declarations: [
ShellComponent, ShellComponent,
SidenavComponent, SidenavComponent,
ThemePickerComponent,
ToolbarComponent, ToolbarComponent,
ContentComponent, ContentComponent,
BreadcrumbComponent BreadcrumbComponent
@@ -150,13 +111,11 @@ import { AuthenticationInterceptor } from './authentication/authentication.inter
MatTreeModule, MatTreeModule,
MatSortModule, MatSortModule,
MatPaginatorModule, MatPaginatorModule,
MatDividerModule, MatDividerModule
ThemePickerComponent
], ],
providers: [ providers: [
AuthenticationService, AuthenticationService,
AuthenticationGuard, AuthenticationGuard,
I18nService,
HttpCacheService, HttpCacheService,
ProgressBarService, ProgressBarService,
ProgressInterceptor, ProgressInterceptor,
+3 -1
View File
@@ -20,7 +20,9 @@ export function extract(s: string) {
return s; return s;
} }
@Injectable() @Injectable({
providedIn: 'root'
})
export class I18nService { export class I18nService {
defaultLanguage: string; defaultLanguage: string;
-1
View File
@@ -1,6 +1,5 @@
export * from './core.module'; export * from './core.module';
export * from './authentication/authentication.service'; export * from './authentication/authentication.service';
// export * from './authentication/authentication.service.mock';
export * from './authentication/authentication.guard'; export * from './authentication/authentication.guard';
export * from './i18n.service'; export * from './i18n.service';
export * from './http/http.service'; export * from './http/http.service';
@@ -14,7 +14,7 @@
font-weight: 400; font-weight: 400;
} }
.separator:before { .separator::before {
content: ""; content: "";
border: 1.5px solid #000; border: 1.5px solid #000;
margin: 0 0.6rem; margin: 0 0.6rem;
@@ -30,19 +30,17 @@
a { a {
text-decoration: none; text-decoration: none;
color: #0275d8; 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 { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router'; import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
/** rxjs Imports */
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
/** Custom Model */
import { Breadcrumb } from './breadcrumb.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({ @Component({
selector: 'mifosx-breadcrumb', selector: 'mifosx-breadcrumb',
templateUrl: './breadcrumb.component.html', templateUrl: './breadcrumb.component.html',
@@ -12,19 +47,26 @@ import { Breadcrumb } from './breadcrumb.model';
}) })
export class BreadcrumbComponent implements OnInit { export class BreadcrumbComponent implements OnInit {
/** Array of breadcrumbs. */
breadcrumbs: Breadcrumb[]; breadcrumbs: Breadcrumb[];
/**
* Generates the breadcrumbs.
* @param {ActivatedRoute} activatedRoute Activate Route.
* @param {Router} router Router for navigation.
*/
constructor(private activatedRoute: ActivatedRoute, constructor(private activatedRoute: ActivatedRoute,
private router: Router) { private router: Router) {
this.generateBreadcrumbs(); this.generateBreadcrumbs();
} }
generateBreadcrumbs() { ngOnInit() {
const routeDataBreadcrumb = 'breadcrumb'; }
const routeParamBreadcrumb = 'routeParamBreadcrumb';
const routeResolveBreadcrumb = 'routeResolveBreadcrumb';
const routeAddBreadcrumbLink = 'addBreadcrumbLink';
/**
* Generates the array of breadcrumbs for the visited route.
*/
generateBreadcrumbs() {
const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd)); const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd));
onNavigationEnd.subscribe(() => { onNavigationEnd.subscribe(() => {
@@ -86,7 +128,4 @@ export class BreadcrumbComponent implements OnInit {
}); });
} }
ngOnInit() {
}
} }
@@ -1,3 +1,6 @@
/**
* Breadcrumb model.
*/
export interface Breadcrumb { export interface Breadcrumb {
label: string; label: string;
url: string; url: string;
@@ -1 +1,2 @@
<!-- Display the content for the visited route. -->
<router-outlet></router-outlet> <router-outlet></router-outlet>
@@ -1,5 +1,9 @@
/** Angular Imports */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
/**
* Content component.
*/
@Component({ @Component({
selector: 'mifosx-content', selector: 'mifosx-content',
templateUrl: './content.component.html', templateUrl: './content.component.html',
+9
View File
@@ -1,5 +1,7 @@
<!-- Application Shell -->
<mat-sidenav-container id="mifosx-shell-container" autosize> <mat-sidenav-container id="mifosx-shell-container" autosize>
<!-- Sidenav -->
<mat-sidenav <mat-sidenav
#sidenav #sidenav
class="sidebar-panel" class="sidebar-panel"
@@ -15,13 +17,20 @@
</mat-sidenav> </mat-sidenav>
<!-- Content -->
<mat-sidenav-content> <mat-sidenav-content>
<!-- Toolbar -->
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar> <mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
<!-- Progress Bar -->
<mat-progress-bar [mode]="progressBarMode"></mat-progress-bar> <mat-progress-bar [mode]="progressBarMode"></mat-progress-bar>
<!-- Breadcrumb -->
<mifosx-breadcrumb></mifosx-breadcrumb> <mifosx-breadcrumb></mifosx-breadcrumb>
<!-- Content -->
<mifosx-content></mifosx-content> <mifosx-content></mifosx-content>
<!-- Footer -->
<mifosx-footer></mifosx-footer>
</mat-sidenav-content> </mat-sidenav-content>
+2 -3
View File
@@ -3,10 +3,10 @@
height: 100vh; height: 100vh;
.sidebar-panel { .sidebar-panel {
background-color: white; // TODO: Theme Configuration background-color: #fff;
min-height: 100vh; min-height: 100vh;
overflow: hidden; 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 { .sidebar-full {
@@ -16,5 +16,4 @@
.sidebar-compact { .sidebar-compact {
width: 4rem; width: 4rem;
} }
} }
+1 -13
View File
@@ -1,10 +1,5 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 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'; import { ShellComponent } from './shell.component';
describe('ShellComponent', () => { describe('ShellComponent', () => {
@@ -13,14 +8,7 @@ describe('ShellComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ declarations: [ ShellComponent ]
RouterTestingModule,
TranslateModule.forRoot(),
CoreModule
],
providers: [
{ provide: AuthenticationService, useClass: MockAuthenticationService }
]
}) })
.compileComponents(); .compileComponents();
})); }));
+33 -8
View File
@@ -1,10 +1,17 @@
/** Angular Imports */
import { Component, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core'; import { Component, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs'; /** rxjs Imports */
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ProgressBarService } from '../progress-bar.service';
/** Custom Services */
import { ProgressBarService } from '../progress-bar/progress-bar.service';
/**
* Shell component.
*/
@Component({ @Component({
selector: 'mifosx-shell', selector: 'mifosx-shell',
templateUrl: './shell.component.html', templateUrl: './shell.component.html',
@@ -12,30 +19,48 @@ import { ProgressBarService } from '../progress-bar.service';
}) })
export class ShellComponent implements OnInit, OnDestroy { export class ShellComponent implements OnInit, OnDestroy {
/** Subscription to breakpoint observer for handset. */
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset) isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe( .pipe(
map(result => result.matches) map(result => result.matches)
); );
sidenavCollapsed = false; /** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
/** Progress bar mode. */
progressBarMode: string; 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, constructor(private breakpointObserver: BreakpointObserver,
private progressBarService: ProgressBarService, private progressBarService: ProgressBarService,
private cdr: ChangeDetectorRef) { } private cdr: ChangeDetectorRef) { }
/**
* Subscribes to progress bar to update its mode.
*/
ngOnInit() { ngOnInit() {
this.sidenavCollapsed = true;
this.progressBar$ = this.progressBarService.updateProgressBar.subscribe((mode: string) => { this.progressBar$ = this.progressBarService.updateProgressBar.subscribe((mode: string) => {
this.progressBarMode = mode; this.progressBarMode = mode;
this.cdr.detectChanges(); 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) { toggleCollapse($event: boolean) {
this.sidenavCollapsed = $event; this.sidenavCollapsed = $event;
} }
/**
* Unsubscribes from progress bar.
*/
ngOnDestroy() { ngOnDestroy() {
this.progressBar$.unsubscribe(); this.progressBar$.unsubscribe();
} }
@@ -6,6 +6,7 @@
<div class="navigation-panel" fxLayout="column"> <div class="navigation-panel" fxLayout="column">
<!-- TODO: Allow customization by organization -->
<div class="app-brand"> <div class="app-brand">
<img src="assets/images/MifosX_logo.png" alt="app-logo" class="app-logo"> <img src="assets/images/MifosX_logo.png" alt="app-logo" class="app-logo">
<span class="app-logo-text text-muted">Mifos</span> <span class="app-logo-text text-muted">Mifos</span>
@@ -13,41 +14,43 @@
<div class="app-user"> <div class="app-user">
<div class="app-user-photo"> <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"> <img src="assets/images/user_placeholder.png" alt="app-user-photo">
</div> </div>
<span class="app-user-name mb-05"> <span class="app-user-name mb-05">
Abhay Chawla {{ username }}
</span> </span>
<div class="app-user-controls"> <div class="app-user-controls">
<button class="text-muted xs-button" mat-icon-button matTooltip="Settings" routerLink="/settings"> <button class="text-muted xs-button" mat-icon-button matTooltip="Settings" [routerLink]="['/settings']">
<mat-icon>settings</mat-icon> <fa-icon icon="cog"></fa-icon>
</button> </button>
<button class="text-muted xs-button" mat-icon-button matTooltip="Sign Out" (click)="logout()"> <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> </button>
</div> </div>
</div> </div>
<div class="app-sidenav"> <div class="app-sidenav">
<!-- TODO: Generate frequently accessed routes dynamically -->
<div class="icon-menu"> <div class="icon-menu">
<div class="mt-1 mb-1 nav-item-sep"> <div class="mt-1 mb-1 nav-item-sep">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<span class="text-muted icon-menu-title">Frequently Accessed</span> <span class="text-muted icon-menu-title">Frequently Accessed</span>
</div> </div>
<div class="icon-menu-item"> <div class="icon-menu-item">
<button mat-raised-button matTooltip="Home"> <button mat-raised-button matTooltip="Home" [routerLink]="['/home']">
<mat-icon>home</mat-icon> <fa-icon icon="home" size="lg"></fa-icon>
</button> </button>
</div> </div>
<div class="icon-menu-item"> <div class="icon-menu-item">
<button mat-raised-button matTooltip="Dashboard"> <button mat-raised-button matTooltip="Dashboard" [routerLink]="['/dashboard']">
<mat-icon>dashboard</mat-icon> <fa-icon icon="tachometer-alt" size="lg"></fa-icon>
</button> </button>
</div> </div>
<div class="icon-menu-item"> <div class="icon-menu-item">
<button mat-raised-button matTooltip="Navigation"> <button mat-raised-button matTooltip="Navigation" [routerLink]="['/navigation']">
<mat-icon>navigation</mat-icon> <fa-icon icon="location-arrow" size="lg"></fa-icon>
</button> </button>
</div> </div>
</div> </div>
@@ -58,14 +61,36 @@
</div> </div>
<mat-nav-list> <mat-nav-list>
<mat-list-item routerLink="/dashboard"> <mat-list-item [routerLink]="['/dashboard']">
<mat-icon matListIcon>dashboard</mat-icon> <mat-icon matListIcon>
<fa-icon icon="tachometer-alt" size="sm"></fa-icon>
</mat-icon>
<a matLine>Dashboard</a> <a matLine>Dashboard</a>
</mat-list-item> </mat-list-item>
<mat-list-item> <mat-list-item [routerLink]="['/navigation']">
<mat-icon matListIcon>navigation</mat-icon> <mat-icon matListIcon>
<fa-icon icon="location-arrow" size="sm"></fa-icon>
</mat-icon>
<a matLine>Navigation</a> <a matLine>Navigation</a>
</mat-list-item> </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> </mat-nav-list>
</div> </div>
+125 -102
View File
@@ -1,117 +1,140 @@
img { .sidebar-full,
max-width: 100%; .sidebar-compact {
} .navigation-panel {
position: absolute;
.navigation-panel { left: 0;
position: absolute; width: 100%;
left: 0; height: 100%;
width: 100%; padding-top: 64px;
height: 100%; background: hsla(0, 0%, 100%, 0.95);
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%;
} }
img { img {
max-width: 100%;
}
.app-brand {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
position: fixed;
width: 100%; width: 100%;
height: 100%; max-width: 15rem;
border-radius: 50%; height: 64px;
} line-height: 64px;
top: 0;
left: 0;
z-index: 9999;
color: #444;
.app-user-name { .app-logo {
display: block; width: auto;
font-size: .875rem; height: 42px;
font-weight: 300; margin-right: 8px;
color: rgba(0,0,0,.96);
margin-left: -2px;
}
}
.app-sidenav {
.icon-menu {
padding: 0 24px;
opacity: 1;
.mat-divider {
margin: 0 -24px;
} }
.icon-menu-title { .app-logo-text {
padding-left: 0; width: auto;
margin-left: -8px; 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 { img {
display: inline-block; width: 100%;
height: 100%;
border-radius: 50%;
}
button { .app-user-name {
min-width: auto; display: block;
margin: 4px; 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 { .nav-item-sep {
padding: 0; span {
margin: 1rem 0; padding: 16px 0 0 16px;
display: block;
font-size: 12px;
}
} }
mat-nav-list { .app-sidenav {
margin: 0; .icon-menu {
padding: 0; padding: 0 24px;
list-style: none; opacity: 1;
}
}
.nav-item-sep { .mat-divider {
span { margin: 0 -24px;
padding: 16px 0 0 16px; }
display: block;
font-size: 12px; .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 { .app-sidenav {
.nav-item-sep {
span {
display: none;
}
}
.icon-menu { .icon-menu {
padding: 8px 0 0; padding: 8px 0 0;
width: 4rem; 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 { Component, OnInit, Input } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
/** Custom Services */
import { AuthenticationService } from '../../authentication/authentication.service'; import { AuthenticationService } from '../../authentication/authentication.service';
/**
* Sidenav component.
*/
@Component({ @Component({
selector: 'mifosx-sidenav', selector: 'mifosx-sidenav',
templateUrl: './sidenav.component.html', templateUrl: './sidenav.component.html',
@@ -10,14 +15,29 @@ import { AuthenticationService } from '../../authentication/authentication.servi
}) })
export class SidenavComponent implements OnInit { export class SidenavComponent implements OnInit {
/** True if sidenav is in collapsed state. */
@Input() sidenavCollapsed: boolean; @Input() sidenavCollapsed: boolean;
/** Username of authenticated user. */
username: string;
/**
* @param {Router} router Router for navigation.
* @param {AuthenticationService} authenticationService Authentication Service.
*/
constructor(private router: Router, constructor(private router: Router,
private authenticationService: AuthenticationService) { } private authenticationService: AuthenticationService) { }
/**
* Sets the username of the authenticated user.
*/
ngOnInit() { ngOnInit() {
const credentials = this.authenticationService.getCredentials();
this.username = credentials.username;
} }
/**
* Logs out the authenticated user and redirects to login page.
*/
logout() { logout() {
this.authenticationService.logout() this.authenticationService.logout()
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); .subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
@@ -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"> <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>
<button mat-icon-button *ngIf="sidenav.opened" (click)="toggleCollapse()" matTooltip="Toggle Collapse" fxHide.lt-md="true"> <button mat-icon-button *ngIf="sidenav.opened" (click)="toggleSidenavCollapse()" matTooltip="Toggle Collapse" fxHide.lt-md="true">
<mat-icon *ngIf="!sidenavCollapsed">chevron_left</mat-icon> <fa-icon *ngIf="!sidenavCollapsed" icon="chevron-left"></fa-icon>
<mat-icon *ngIf="sidenavCollapsed">chevron_right</mat-icon> <fa-icon *ngIf="sidenavCollapsed" icon="chevron-right"></fa-icon>
</button> </button>
<nav mat-tab-nav-bar backgroundColor="primary" class="ml-1"> <nav mat-tab-nav-bar backgroundColor="primary" class="ml-1">
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="institutionMenu" #institutionMenuTrigger="matMenuTrigger"> <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 Institution
</a> </a>
<span fxHide.lt-lg="true"> <span fxHide.lt-lg="true">
<a mat-tab-link class="tab-link" [routerLink]="['/accounting']"> <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 Accounting
</a> </a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="reportsMenu" #reportsMenuTrigger="matMenuTrigger" > <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 Reports
</a> </a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="adminMenu" #adminMenuTrigger="matMenuTrigger"> <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 Admin
</a> </a>
<a mat-tab-link class="tab-link" [matMenuTriggerFor]="selfServiceMenu" #selfServiceMenuTrigger="matMenuTrigger"> <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 Self Service
</a> </a>
@@ -41,73 +41,90 @@
<span fxFlex="auto"></span> <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]> <div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" [@fadeInOut]>
<mat-form-field class="search"> <mat-form-field class="search">
<input matInput type="text" placeholder="Search"> <mat-label>Search</mat-label>
<input matInput type="text">
</mat-form-field> </mat-form-field>
</div> </div>
<button mat-icon-button class="ml-1" (click)="toggleVisibility()" fxHide.lt-sm="true"> <button mat-icon-button class="ml-1" (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
<mat-icon>search</mat-icon> <fa-icon icon="search" size="lg"></fa-icon>
</button> </button>
<mat-select placeholder="Language" [(value)]="selectedLanguage" class="ml-1 language" fxHide.lt-md> <mifosx-language-selector class="ml-1 language" fxHide.lt-md></mifosx-language-selector>
<mat-option value="English">English</mat-option>
<mat-option value="Hindi">Hindi</mat-option>
</mat-select>
<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"> <button mat-icon-button class="ml-1" matTooltip="Notifications">
<mat-icon>notifications</mat-icon> <fa-icon icon="bell" size="lg"></fa-icon>
</button> </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"> <img src="assets/images/user_placeholder.png">
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false"> <mat-menu #institutionMenu="matMenu" [overlapTrigger]="false">
<span> <button mat-menu-item routerLink="/clients">Clients</button>
<button mat-menu-item routerLink="/clients">Clients</button> <button mat-menu-item routerLink="/groups">Groups</button>
<button mat-menu-item routerLink="/groups">Groups</button> <button mat-menu-item routerLink="/centers">Centers</button>
<button mat-menu-item routerLink="/centers">Centers</button> <span fxHide.lg>
<span fxHide.lg> <button mat-menu-item [routerLink]="['/accounting']">Accounting</button>
<button mat-menu-item>Accounting</button> <button mat-menu-item [matMenuTriggerFor]="reportsMenu">Reports</button>
<button mat-menu-item [matMenuTriggerFor]="reportsMenu">Reports</button> <button mat-menu-item [matMenuTriggerFor]="adminMenu">Admin</button>
<button mat-menu-item [matMenuTriggerFor]="adminMenu">Admin</button> <button mat-menu-item [matMenuTriggerFor]="selfServiceMenu">Self Service</button>
<button mat-menu-item [matMenuTriggerFor]="selfServiceMenu">Self Service</button>
</span>
</span> </span>
</mat-menu> </mat-menu>
<!-- Toolbar Menus -->
<mat-menu #reportsMenu="matMenu" [overlapTrigger]="false"> <mat-menu #reportsMenu="matMenu" [overlapTrigger]="false">
<span (mouseleave)="reportsMenuTrigger.closeMenu()"> <button mat-menu-item>All</button>
<button mat-menu-item>All</button> <button mat-menu-item>Clients</button>
<button mat-menu-item>Clients</button> <button mat-menu-item>Loans</button>
<button mat-menu-item>Loans</button> <button mat-menu-item>Savings</button>
<button mat-menu-item>Savings</button> <button mat-menu-item>Funds</button>
<button mat-menu-item>Funds</button> <button mat-menu-item>Accounting</button>
<button mat-menu-item>Accounting</button> <button mat-menu-item>XBRL</button>
<button mat-menu-item>XBRL</button>
</span>
</mat-menu> </mat-menu>
<mat-menu #adminMenu="matMenu" [overlapTrigger]="false"> <mat-menu #adminMenu="matMenu" [overlapTrigger]="false">
<span (mouseleave)="adminMenuTrigger.closeMenu()"> <button mat-menu-item>Users</button>
<button mat-menu-item>Users</button> <button mat-menu-item>Organisation</button>
<button mat-menu-item>Organisation</button> <button mat-menu-item>System</button>
<button mat-menu-item>System</button> <button mat-menu-item>Products</button>
<button mat-menu-item>Products</button> <button mat-menu-item>Templates</button>
<button mat-menu-item>Templates</button>
</span>
</mat-menu> </mat-menu>
<mat-menu #selfServiceMenu="matMenu" [overlapTrigger]="false"> <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/users">User Management</button> <button mat-menu-item [routerLink]="['/self-service/app-configuration']">App Configuration</button>
<button mat-menu-item routerLink="/self-service/app-configuration">App Configuration</button> <button mat-menu-item>Task Management</button>
<button mat-menu-item>Task Management</button> </mat-menu>
</span>
<!-- 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> </mat-menu>
@@ -1,45 +1,40 @@
mat-form-field { #mifosx-toolbar {
font-size: 1rem;
margin-top: 6px;
}
.toolbar {
position: relative; 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 { .tab-link {
min-width: auto; min-width: auto;
padding: 0 16px; padding: 0 1rem;
} }
.search-bar { .search-bar {
mat-form-field { .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; 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 { 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 { 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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
/** Custom Services */
import { AuthenticationService } from '../../authentication/authentication.service';
/**
* Toolbar component.
*/
@Component({ @Component({
selector: 'mifosx-toolbar', selector: 'mifosx-toolbar',
templateUrl: './toolbar.component.html', templateUrl: './toolbar.component.html',
@@ -24,45 +33,70 @@ import { map } from 'rxjs/operators';
}) })
export class ToolbarComponent implements OnInit { export class ToolbarComponent implements OnInit {
/** Subscription to breakpoint observer for handset. */
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset) isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe( .pipe(
map(result => result.matches) map(result => result.matches)
); );
isHandset: boolean; /** Sets the initial visibility of search input as hidden. Visible if true. */
searchVisible = false; searchVisible = false;
selectedLanguage = 'English'; /** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = false; sidenavCollapsed = true;
/** Instance of sidenav. */
@Input() sidenav: MatSidenav; @Input() sidenav: MatSidenav;
/** Sidenav collapse event. */
@Output() collapse = new EventEmitter<boolean>(); @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() { ngOnInit() {
this.sidenavCollapsed = !this.sidenavCollapsed;
this.isHandset$.subscribe(isHandset => { this.isHandset$.subscribe(isHandset => {
this.isHandset = isHandset; if (isHandset && this.sidenavCollapsed) {
this.toggleSidenavCollapse(false);
}
}); });
} }
/**
* Toggles the current state of sidenav.
*/
toggleSidenav() { toggleSidenav() {
this.sidenav.toggle(); 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); this.collapse.emit(this.sidenavCollapsed);
} }
toggleVisibility() { /**
* Toggles the visibility of search input with fadeInOut animation.
*/
toggleSearchVisibility() {
this.searchVisible = !this.searchVisible; this.searchVisible = !this.searchVisible;
} }
/* onMouseEnter(menuTrigger: MatMenuTrigger) { /**
if (!this.isHandset) { * Logs out the authenticated user and redirects to login page.
menuTrigger.openMenu(); */
} logout() {
this.authenticationService.logout()
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
} }
*/
} }
+1 -1
View File
@@ -55,7 +55,7 @@
</div> </div>
<!-- Contact Information/Resources (hidden on smaller screens) --> <!-- Contact Information/Resources (hidden on smaller screens) -->
<mat-list fxHide.lt-md fxLayout="row" fxFlexAlign="center" fxFlex="3 0 auto" class="information-list"> <mat-list fxHide.lt-lg fxLayout="row" fxFlexAlign="center" fxFlex="3 0 auto" class="information-list">
<mat-list-item><button mat-button [matMenuTriggerFor]="resourcesMenu">Resources</button></mat-list-item> <mat-list-item><button mat-button [matMenuTriggerFor]="resourcesMenu">Resources</button></mat-list-item>
<mat-list-item><button mat-button [matMenuTriggerFor]="communityMenu">Community</button></mat-list-item> <mat-list-item><button mat-button [matMenuTriggerFor]="communityMenu">Community</button></mat-list-item>
<mat-list-item><button mat-button [matMenuTriggerFor]="contributeMenu">Contribute</button></mat-list-item> <mat-list-item><button mat-button [matMenuTriggerFor]="contributeMenu">Contribute</button></mat-list-item>
+1 -1
View File
@@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component'; import { LoginComponent } from './login.component';
describe('LanguageSelectorComponent', () => { describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
+1 -1
View File
@@ -1,6 +1,6 @@
#footer { #footer {
.divider { .divider {
margin: 1rem 1rem 0.5rem; margin: 2.5rem 1rem 0.5rem;
width: 4rem; width: 4rem;
} }
+36
View File
@@ -5,25 +5,61 @@ import { NgModule } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core'; import { library } from '@fortawesome/fontawesome-svg-core';
import { import {
faBars,
faBell,
faChartBar,
faCheckCircle, faCheckCircle,
faChevronLeft,
faChevronRight,
faCog,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileUpload, faFileUpload,
faFillDrip,
faHome,
faLocationArrow,
faLock, faLock,
faMoneyBillAlt,
faPlus,
faSearch, faSearch,
faShieldAlt,
faSignOutAlt,
faSitemap,
faSync,
faTachometerAlt,
faUniversity,
faUserCircle, faUserCircle,
faUsers,
faUserShield faUserShield
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
/** Add icons to the library for convenient access in other components. */ /** Add icons to the library for convenient access in other components. */
library.add( library.add(
faBars,
faBell,
faChartBar,
faCheckCircle, faCheckCircle,
faChevronLeft,
faChevronRight,
faCog,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileUpload, faFileUpload,
faFillDrip,
faHome,
faLocationArrow,
faLock, faLock,
faMoneyBillAlt,
faPlus,
faSearch, faSearch,
faShieldAlt,
faSignOutAlt,
faSitemap,
faSync,
faTachometerAlt,
faUniversity,
faUserCircle, faUserCircle,
faUsers,
faUserShield faUserShield
); );
@@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
/** Custom Services */ /** Custom Services */
import { I18nService } from 'app/core'; import { I18nService } from '../../core/i18n.service';
/** /**
* Language selector component. * Language selector component.
+4 -1
View File
@@ -8,6 +8,7 @@ import { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
import { FileUploadComponent } from './file-upload/file-upload.component'; import { FileUploadComponent } from './file-upload/file-upload.component';
import { FooterComponent } from './footer/footer.component'; import { FooterComponent } from './footer/footer.component';
import { LanguageSelectorComponent } from './language-selector/language-selector.component'; import { LanguageSelectorComponent } from './language-selector/language-selector.component';
import { ThemePickerComponent } from './theme-picker/theme-picker.component';
/** Custom Modules */ /** Custom Modules */
import { IconsModule } from './icons.module'; import { IconsModule } from './icons.module';
@@ -29,12 +30,14 @@ import { MaterialModule } from './material.module';
DeleteDialogComponent, DeleteDialogComponent,
FileUploadComponent, FileUploadComponent,
FooterComponent, FooterComponent,
LanguageSelectorComponent LanguageSelectorComponent,
ThemePickerComponent
], ],
exports: [ exports: [
FileUploadComponent, FileUploadComponent,
FooterComponent, FooterComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
ThemePickerComponent,
CommonModule, CommonModule,
IconsModule, IconsModule,
MaterialModule, MaterialModule,
@@ -1,18 +1,30 @@
/** Angular Imports */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
/**
* Theme manager service.
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ThemeManagerService { export class ThemeManagerService {
/** Stylesheet class for the theme of the application. */
private themeManagerClass = 'theme-manager'; private themeManagerClass = 'theme-manager';
constructor() { } constructor() { }
/**
* Sets a new theme for the application.
* @param {string} href Link to stylesheet.
*/
setTheme(href: string) { setTheme(href: string) {
this.getLinkElement().setAttribute('href', href); this.getLinkElement().setAttribute('href', href);
} }
/**
* Removes the current theme of the application.
*/
removeTheme() { removeTheme() {
const existingLinkElement = this.getExistingLinkElement(); const existingLinkElement = this.getExistingLinkElement();
if (existingLinkElement) { if (existingLinkElement) {
@@ -20,14 +32,23 @@ export class ThemeManagerService {
} }
} }
/**
* @returns link element of stylesheet
*/
private getLinkElement() { private getLinkElement() {
return this.getExistingLinkElement() || this.createLinkElement(); return this.getExistingLinkElement() || this.createLinkElement();
} }
/**
* @returns existing link element of stylesheet with theme manager class
*/
private getExistingLinkElement() { private getExistingLinkElement() {
return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`); return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`);
} }
/**
* @returns newly created link element of stylesheet with theme manager class
*/
private createLinkElement() { private createLinkElement() {
const linkElement = document.createElement('link'); const linkElement = document.createElement('link');
linkElement.setAttribute('rel', 'stylesheet'); linkElement.setAttribute('rel', 'stylesheet');
@@ -1,5 +1,5 @@
<button mat-icon-button [matMenuTriggerFor]="themeMenu" matTooltip="Color Schemes"> <button mat-icon-button [matMenuTriggerFor]="themeMenu" matTooltip="Color Schemes">
<mat-icon>format_color_fill</mat-icon> <fa-icon icon="fill-drip" size="lg"></fa-icon>
</button> </button>
<mat-menu class="mifosx-theme-picker-menu" #themeMenu="matMenu" x-position="before"> <mat-menu class="mifosx-theme-picker-menu" #themeMenu="matMenu" x-position="before">
@@ -7,7 +7,7 @@
<mat-grid-tile *ngFor="let theme of themes"> <mat-grid-tile *ngFor="let theme of themes">
<div mat-menu-item (click)="installTheme(theme)"> <div mat-menu-item (click)="installTheme(theme)">
<div class="mifosx-theme-picker-swatch"> <div class="mifosx-theme-picker-swatch">
<mat-icon class="mifosx-theme-chosen-icon" *ngIf="currentTheme.href === theme.href">check_circle</mat-icon> <fa-icon class="mifosx-theme-chosen-icon" *ngIf="currentTheme.href === theme.href" icon="check-circle" size="lg"></fa-icon>
<div class="mifosx-theme-picker-primary" [style.background]="theme.primary"></div> <div class="mifosx-theme-picker-primary" [style.background]="theme.primary"></div>
<div class="mifosx-theme-picker-accent" [style.background]="theme.accent"></div> <div class="mifosx-theme-picker-accent" [style.background]="theme.accent"></div>
</div> </div>
@@ -26,19 +26,20 @@ $theme-picker-accent-stripe-size: 6px;
.mifosx-theme-chosen-icon { .mifosx-theme-chosen-icon {
color: white; color: white;
position: absolute; position: absolute;
left: 50%; top: 50%; left: 50%;
top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
&::after { &::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(0,0,0,.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 50%; border-radius: 50%;
} }
} }
@@ -1,8 +1,17 @@
/** Angular Imports */
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
/** Custom Model */
import { Theme } from './theme.model'; import { Theme } from './theme.model';
/** Custom Services */
import { ThemeStorageService } from './theme-storage.service'; import { ThemeStorageService } from './theme-storage.service';
/**
* Theme picker component.
*
* TODO: Customization of theme for every component and custom background with darker contrast.
*/
@Component({ @Component({
selector: 'mifosx-theme-picker', selector: 'mifosx-theme-picker',
templateUrl: './theme-picker.component.html', templateUrl: './theme-picker.component.html',
@@ -11,6 +20,7 @@ import { ThemeStorageService } from './theme-storage.service';
}) })
export class ThemePickerComponent implements OnInit { export class ThemePickerComponent implements OnInit {
/** Default theme for the application. */
currentTheme: Theme = { currentTheme: Theme = {
href: 'denim-yellowgreen.css', href: 'denim-yellowgreen.css',
primary: '#1074B9', primary: '#1074B9',
@@ -18,6 +28,7 @@ export class ThemePickerComponent implements OnInit {
isDark: false, isDark: false,
isDefault: true isDefault: true
}; };
/** Available themes for the application. */
themes = [ themes = [
this.currentTheme, this.currentTheme,
{ {
@@ -38,7 +49,7 @@ export class ThemePickerComponent implements OnInit {
accent: '#FFC107', accent: '#FFC107',
isDark: false isDark: false
}, },
{ // TODO: Remove Dark Themes {
href: 'pink-bluegrey.css', href: 'pink-bluegrey.css',
primary: '#E91E63', primary: '#E91E63',
accent: '#607D8B', accent: '#607D8B',
@@ -52,8 +63,14 @@ export class ThemePickerComponent implements OnInit {
} }
]; ];
/**
* @param {ThemeStorageService} themeStorageService Theme Storage Service.
*/
constructor(public themeStorageService: ThemeStorageService) { } constructor(public themeStorageService: ThemeStorageService) { }
/**
* Initializes the theme for the application.
*/
ngOnInit() { ngOnInit() {
const theme = this.themeStorageService.getTheme(); const theme = this.themeStorageService.getTheme();
if (theme) { if (theme) {
@@ -61,6 +78,10 @@ export class ThemePickerComponent implements OnInit {
} }
} }
/**
* Installs a new theme for the application.
* @param {Theme} theme
*/
installTheme(theme: Theme) { installTheme(theme: Theme) {
this.currentTheme = theme; this.currentTheme = theme;
this.themeStorageService.installTheme(theme); this.themeStorageService.installTheme(theme);
@@ -1,33 +1,60 @@
/** Angular Imports */
import {Injectable, EventEmitter} from '@angular/core'; import {Injectable, EventEmitter} from '@angular/core';
/** Custom Model */
import { Theme } from './theme.model'; import { Theme } from './theme.model';
/** Custom Services */
import { ThemeManagerService } from './theme-manager.service'; import { ThemeManagerService } from './theme-manager.service';
/**
* Theme storage service.
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ThemeStorageService { export class ThemeStorageService {
/** Key to store current theme of application in local storage. */
private themeStorageKey = 'mifosXTheme'; private themeStorageKey = 'mifosXTheme';
/** Theme update event. */
onThemeUpdate: EventEmitter<Theme>; onThemeUpdate: EventEmitter<Theme>;
/**
* Initializes theme update event.
* @param {ThemeManagerService} themeManagerService
*/
constructor(public themeManagerService: ThemeManagerService) { constructor(public themeManagerService: ThemeManagerService) {
this.onThemeUpdate = new EventEmitter<Theme>(); this.onThemeUpdate = new EventEmitter<Theme>();
} }
/**
* Stores current theme in local storage and emits a theme update event.
* @param mifosXTheme
*/
storeTheme(mifosXTheme: Theme) { storeTheme(mifosXTheme: Theme) {
localStorage.setItem(this.themeStorageKey, JSON.stringify(mifosXTheme)); localStorage.setItem(this.themeStorageKey, JSON.stringify(mifosXTheme));
this.onThemeUpdate.emit(mifosXTheme); this.onThemeUpdate.emit(mifosXTheme);
} }
/**
* @returns current theme from local storage
*/
getTheme(): Theme { getTheme(): Theme {
return JSON.parse(localStorage.getItem(this.themeStorageKey)); return JSON.parse(localStorage.getItem(this.themeStorageKey));
} }
/**
* Removes current theme from local storage.
*/
clearTheme() { clearTheme() {
localStorage.removeItem(this.themeStorageKey); localStorage.removeItem(this.themeStorageKey);
} }
/**
* Sets a new theme for the application and stores in local storage.
* @param {Theme} theme
*/
installTheme(theme: Theme) { installTheme(theme: Theme) {
if (theme.isDefault) { if (theme.isDefault) {
this.themeManagerService.removeTheme(); this.themeManagerService.removeTheme();
@@ -1,3 +1,6 @@
/**
* Theme model.
*/
export interface Theme { export interface Theme {
href: string; href: string;
primary: string; primary: string;
+2 -2
View File
@@ -7,9 +7,9 @@ import { TranslateService } from '@ngx-translate/core';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators'; import { filter, map, mergeMap } from 'rxjs/operators';
import { environment } from '../environments/environment'; import { environment } from 'environments/environment';
import { Logger, I18nService } from './core'; import { Logger, I18nService } from './core';
import { ThemeStorageService } from './core/shell/toolbar/theme-picker/theme-storage.service'; import { ThemeStorageService } from './shared/theme-picker/theme-storage.service';
import { AlertService } from './core/alert.service'; import { AlertService } from './core/alert.service';
const log = new Logger('MifosX'); const log = new Logger('MifosX');
+30 -46
View File
@@ -3,56 +3,40 @@
@import "width-height"; @import "width-height";
@import "z-index"; @import "z-index";
body, html { body,
width: 100%; html {
height: 100%; width: 100%;
position: relative; height: 100%;
overflow: hidden; position: relative;
-webkit-tap-highlight-color: transparent; overflow: hidden;
min-height: 100%; -webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased; min-height: 100%;
} -webkit-font-smoothing: antialiased;
}
html { html {
font-size: 16px; font-size: 16px;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-weight: 400; font-weight: 400;
font-size: .875rem; font-size: .875rem;
color: rgba(0,0,0,.87); color: rgba(0,0,0,.87);
line-height: 1.5; line-height: 1.5;
font-family: Roboto, Helvetica Neue, sans-serif; font-family: Roboto, Helvetica Neue, sans-serif;
} }
div { div {
box-sizing: border-box; box-sizing: border-box;
} }
.container { .container {
margin: 0 auto; margin: 0 auto;
max-width: 74rem; max-width: 74rem;
width: 90%; width: 90%;
} }
.mat-icon-button.xs-button {
height: 20px;
width: 20px;
line-height: 20px;
}
.mat-icon-button.xs-button .mat-icon {
font-size: 16px;
line-height: 20px;
height: 20px;
width: 20px;
}
.text-muted {
color: rgba(0, 0, 0, .54);
}
.mb-05 { .mb-05 {
margin-bottom: .5rem; margin-bottom: .5rem;