refractor: login module (#283)
* Refractor components. * Shift language and footer component to shared module. * Replace mat-icons by font-awesome icons. * Add comments. Fixes #284
This commit is contained in:
+13
-4
@@ -10,9 +10,9 @@
|
|||||||
],
|
],
|
||||||
"selector-attribute-quotes": "always",
|
"selector-attribute-quotes": "always",
|
||||||
"string-quotes": "double",
|
"string-quotes": "double",
|
||||||
"max-nesting-depth": 3,
|
"max-nesting-depth": 5,
|
||||||
"selector-max-compound-selectors": 3,
|
"selector-max-compound-selectors": 5,
|
||||||
"selector-max-specificity": "0,3,2",
|
"selector-max-specificity": "1,3,2",
|
||||||
"declaration-no-important": true,
|
"declaration-no-important": true,
|
||||||
"at-rule-no-vendor-prefix": true,
|
"at-rule-no-vendor-prefix": true,
|
||||||
"media-feature-name-no-vendor-prefix": true,
|
"media-feature-name-no-vendor-prefix": true,
|
||||||
@@ -22,9 +22,18 @@
|
|||||||
"no-empty-source": null,
|
"no-empty-source": null,
|
||||||
"selector-class-pattern": "[a-z-]+",
|
"selector-class-pattern": "[a-z-]+",
|
||||||
"selector-id-pattern": "[a-z-]+",
|
"selector-id-pattern": "[a-z-]+",
|
||||||
"selector-max-id": 0,
|
"selector-max-id": 1,
|
||||||
"selector-no-qualifying-type": true,
|
"selector-no-qualifying-type": true,
|
||||||
"selector-max-universal": 0,
|
"selector-max-universal": 0,
|
||||||
|
"selector-type-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreTypes": [
|
||||||
|
"mat-spinner",
|
||||||
|
"mat-radio-button"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"selector-pseudo-element-no-unknown": [
|
"selector-pseudo-element-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { Injectable, EventEmitter } from '@angular/core';
|
import { Injectable, EventEmitter } from '@angular/core';
|
||||||
|
|
||||||
|
export interface Alert {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AlertService {
|
export class AlertService {
|
||||||
|
|
||||||
public alertEvent: EventEmitter<any>;
|
public alertEvent: EventEmitter<Alert>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.alertEvent = new EventEmitter();
|
this.alertEvent = new EventEmitter();
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(alertEvent: any) {
|
alert(alertEvent: Alert) {
|
||||||
this.alertEvent.emit(alertEvent);
|
this.alertEvent.emit(alertEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export * from './http/error-handler.interceptor';
|
|||||||
export * from './route.service';
|
export * from './route.service';
|
||||||
export * from './route-reusable-strategy';
|
export * from './route-reusable-strategy';
|
||||||
export * from './logger.service';
|
export * from './logger.service';
|
||||||
|
export * from './alert.service';
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()">
|
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
|
||||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
|
||||||
<span matPrefix><mat-icon>account_circle</mat-icon> </span>
|
<mat-form-field fxFlexAlign="center" class="login-input">
|
||||||
<input matInput placeholder="Username" autocomplete="off" formControlName="username">
|
<span matPrefix>
|
||||||
|
<fa-icon icon="user-circle" size="lg"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<mat-label>Username</mat-label>
|
||||||
|
<input matInput type="text" autocomplete="off" formControlName="username">
|
||||||
<mat-error *ngIf="loginForm.controls.username.hasError('required')">
|
<mat-error *ngIf="loginForm.controls.username.hasError('required')">
|
||||||
Username is <strong>required</strong>
|
Username is <strong>required</strong>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
|
||||||
<span matPrefix><mat-icon>lock</mat-icon> </span>
|
<mat-form-field fxFlexAlign="center" class="login-input">
|
||||||
<input matInput type="{{ passwordInputType }}" placeholder="Password" formControlName="password">
|
<span matPrefix>
|
||||||
|
<fa-icon icon="lock" size="lg"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<mat-label>Password</mat-label>
|
||||||
|
<input matInput type="{{ passwordInputType }}" formControlName="password">
|
||||||
<button mat-button *ngIf="loginForm.controls.password.value && !loading" matSuffix mat-icon-button
|
<button mat-button *ngIf="loginForm.controls.password.value && !loading" matSuffix mat-icon-button
|
||||||
aria-label="Clear" (mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
(mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||||
<mat-icon *ngIf="passwordInputType === 'password'">visibility</mat-icon>
|
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
|
||||||
<mat-icon *ngIf="passwordInputType === 'text'">visibility_off</mat-icon>
|
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-error *ngIf="loginForm.controls.password.hasError('required')">
|
<mat-error *ngIf="loginForm.controls.password.hasError('required')">
|
||||||
Password is <strong>required</strong>
|
Password is <strong>required</strong>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-checkbox fxFlexAlign="center" formControlName="remember" class="m-t-10">Remember me</mat-checkbox>
|
<mat-checkbox fxFlexAlign="center" formControlName="remember" class="m-t-10">Remember me</mat-checkbox>
|
||||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!loginForm.valid">
|
|
||||||
|
<button mat-raised-button color="primary" fxFlexAlign="center" class="login-button" [disabled]="!loginForm.valid">
|
||||||
Login
|
Login
|
||||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-button fxFlexAlign="center" class="btn" (click)="forgotPassword()" [disabled]="loading">Forgot Password?</button>
|
|
||||||
|
<button type="button" mat-button fxFlexAlign="center" class="login-button" (click)="forgotPassword()" [disabled]="loading">
|
||||||
|
Forgot Password?
|
||||||
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
.textfield {
|
#login-form {
|
||||||
width: 14rem;
|
.login-input {
|
||||||
margin-bottom: 0.6rem;
|
width: 14rem;
|
||||||
}
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.login-button {
|
||||||
width: 14rem;
|
width: 14rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-spinner {
|
mat-spinner {
|
||||||
z-index: 4;
|
float: right;
|
||||||
float: right;
|
margin: 0.5rem 0;
|
||||||
margin: 0.5rem 0;
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Logger, AuthenticationService } from '../../core';
|
/** Custom Services */
|
||||||
|
import { AuthenticationService } from '../../core';
|
||||||
const log = new Logger('Login');
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login form component.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-login-form',
|
selector: 'mifosx-login-form',
|
||||||
templateUrl: './login-form.component.html',
|
templateUrl: './login-form.component.html',
|
||||||
@@ -14,37 +18,56 @@ const log = new Logger('Login');
|
|||||||
})
|
})
|
||||||
export class LoginFormComponent implements OnInit {
|
export class LoginFormComponent implements OnInit {
|
||||||
|
|
||||||
|
/** Login form group. */
|
||||||
loginForm: FormGroup;
|
loginForm: FormGroup;
|
||||||
|
/** Password input field type. */
|
||||||
passwordInputType: string;
|
passwordInputType: string;
|
||||||
error: string;
|
/** True if loading. */
|
||||||
loading = false;
|
loading = false;
|
||||||
authenticationEvent: any;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormBuilder} formBuilder Form Builder.
|
||||||
|
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||||
|
*/
|
||||||
constructor(private formBuilder: FormBuilder,
|
constructor(private formBuilder: FormBuilder,
|
||||||
private authenticationService: AuthenticationService) { }
|
private authenticationService: AuthenticationService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates login form.
|
||||||
|
*
|
||||||
|
* Initializes password input field type.
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.createLoginForm();
|
this.createLoginForm();
|
||||||
this.passwordInputType = 'password';
|
this.passwordInputType = 'password';
|
||||||
}
|
}
|
||||||
|
|
||||||
public login() {
|
/**
|
||||||
|
* Authenticates the user if the credentials are valid.
|
||||||
|
*/
|
||||||
|
login() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.loginForm.disable();
|
this.loginForm.disable();
|
||||||
this.authenticationService.login(this.loginForm.value)
|
this.authenticationService.login(this.loginForm.value)
|
||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.loginForm.reset();
|
this.loginForm.reset();
|
||||||
this.loginForm.markAsPristine();
|
this.loginForm.markAsPristine();
|
||||||
// Bug: Validation errors won't get removed on reset
|
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||||
this.loginForm.enable();
|
this.loginForm.enable();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
})).subscribe();
|
})).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public forgotPassword() {
|
/**
|
||||||
console.log('TODO: Forgot Password');
|
* TODO: Decision to be taken on providing this feature.
|
||||||
|
*/
|
||||||
|
forgotPassword() {
|
||||||
|
console.log('Forgot Password feature currently unavailable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates login form.
|
||||||
|
*/
|
||||||
private createLoginForm() {
|
private createLoginForm() {
|
||||||
this.loginForm = this.formBuilder.group({
|
this.loginForm = this.formBuilder.group({
|
||||||
'username': ['', Validators.required],
|
'username': ['', Validators.required],
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
/** Transalation Imports */
|
||||||
import { extract } from '../core';
|
import { extract } from '../core';
|
||||||
|
|
||||||
|
/** Custom Components */
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
/** Login Routes */
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
@@ -12,6 +17,11 @@ const routes: Routes = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login Routing Module
|
||||||
|
*
|
||||||
|
* Configures the login routes.
|
||||||
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -1,78 +1,88 @@
|
|||||||
|
<!-- The viewport is divided into two parts having max-width 70% and 30% -->
|
||||||
<div fxLayout="row">
|
<div fxLayout="row">
|
||||||
<!-- I want a row divided into 2 parts 70% and 30% -->
|
|
||||||
|
|
||||||
<!-- In 70% we have a cover pic, org/app name, description, contact information -->
|
<!--
|
||||||
<!-- Column disappers on small screens -->
|
Content in 70%: (TODO: Allow customization by organization)
|
||||||
|
Cover Image
|
||||||
|
Organization/Application Name
|
||||||
|
Organization/Application Description
|
||||||
|
|
||||||
|
This content will be hidden for smaller screens.
|
||||||
|
-->
|
||||||
|
|
||||||
<div fxFlex="70%" fxLayout="column" fxLayoutAlign="center center" fxHide.lt-md class="cover">
|
<div fxFlex="70%" fxLayout="column" fxLayoutAlign="center center" fxHide.lt-md class="cover">
|
||||||
<div class="cover-content">
|
<div class="cover-content">
|
||||||
<h1 class="mat-display-1 cover-title">Mifos</h1>
|
<h1 class="mat-display-1 cover-title">Mifos</h1>
|
||||||
<p class="mat-headline">
|
<p class="mat-headline cover-description">
|
||||||
Mifos X is designed by the <a href="http://www.openmf.org">Mifos Initiative</a>. A <a href="http://mifos.org/resources/community/">global community</a> that aims to speed the elimination of poverty by enabling Organizations to more effectively and efficiently deliver responsible financial services to the world’s poor and unbanked.
|
Mifos X is designed by the <a href="http://www.openmf.org" target="_blank" class="cover-link">Mifos Initiative</a>. A <a href="http://mifos.org/resources/community/" target="_blank" class="cover-link">global community</a> that aims to speed the elimination of poverty by enabling Organizations to more effectively and efficiently deliver responsible financial services to the world’s poor and unbanked.
|
||||||
Sounds interesting? <a href="http://mifos.org/take-action/volunteer/">Get involved!</a>
|
Sounds interesting? <a href="http://mifos.org/take-action/volunteer/" target="_blank" class="cover-link">Get involved!</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- In 30% when gt md otherwise fully covering -->
|
<!--
|
||||||
|
Content in 30%: (TODO: Allow customization of Name, Logo and Contact Information by organization)
|
||||||
|
Language Selector
|
||||||
|
Organization/Application Name with Logo
|
||||||
|
Form
|
||||||
|
Contact Information/Resources
|
||||||
|
Footer
|
||||||
|
-->
|
||||||
|
|
||||||
<div fxLayout="column" fxFlex.gt-md="1 0 30%" class="login-container">
|
<div fxLayout="column" fxFlex.gt-md="1 0 30%" class="login-container">
|
||||||
|
|
||||||
<!-- First row we have language selector in the right of row -->
|
<!-- Language Selector to the extreme right -->
|
||||||
<div fxLayout="row-reverse" fxFlex="1 0 auto">
|
<div fxLayout="row-reverse" fxFlex="1 0 auto">
|
||||||
<mat-form-field class="p-r-10 p-t-10 language-selector">
|
<mifosx-language-selector class="p-r-10 p-t-10"></mifosx-language-selector>
|
||||||
<mat-select placeholder="Language" [(ngModel)]="language">
|
|
||||||
<mat-option *ngFor="let language of languages" [value]="language">
|
|
||||||
{{ language }}
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next we have the logo -->
|
<!-- Logo with Organisation Name -->
|
||||||
<div fxLayout="row" fxLayoutAlign="center center" fxFlex="1 0 auto">
|
<div fxLayout="row" fxLayoutAlign="center center" fxFlex="1 0 auto">
|
||||||
<img src="assets/images/mifos-logo-flat.png" alt="" class="img-container">
|
<img src="assets/images/mifos-logo-flat.png" alt="Mifos X Logo" class="img-container">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next we have the type of form -->
|
<!--
|
||||||
|
Type of Form:
|
||||||
|
Login
|
||||||
|
Reset Password
|
||||||
|
Two Factor Authentication
|
||||||
|
Forgot Password (TODO: Decision to be taken on providing this feature.)
|
||||||
|
-->
|
||||||
<div fxLayout="row" fxFlex="3 0 auto" fxLayoutAlign="center start">
|
<div fxLayout="row" fxFlex="3 0 auto" fxLayoutAlign="center start">
|
||||||
<mifosx-login-form *ngIf="!resetPassword && !twoFactorAuthenticationRequired"></mifosx-login-form>
|
<mifosx-login-form *ngIf="!resetPassword && !twoFactorAuthenticationRequired"></mifosx-login-form>
|
||||||
<mifosx-reset-password *ngIf="resetPassword"></mifosx-reset-password>
|
<mifosx-reset-password *ngIf="resetPassword"></mifosx-reset-password>
|
||||||
<mifosx-two-factor-authentication *ngIf="twoFactorAuthenticationRequired"></mifosx-two-factor-authentication>
|
<mifosx-two-factor-authentication *ngIf="twoFactorAuthenticationRequired"></mifosx-two-factor-authentication>
|
||||||
<!-- TODO: Discuss about providing a feature of Forgot password -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Contact Information/Resources (hidden on smaller screens) -->
|
||||||
<mat-list fxHide.lt-md fxLayout="row" fxFlexAlign="center" fxFlex="3 0 auto" class="list">
|
<mat-list fxHide.lt-md 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>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
||||||
<div fxLayout="column" fxFlexAlign="center" fxFlex="1 0 auto">
|
<!-- Footer -->
|
||||||
<mat-divider class="divider"></mat-divider>
|
<mifosx-footer></mifosx-footer>
|
||||||
<p fxFlexAlign="center">Version {{ version }}</p>
|
|
||||||
<p fxFlexAlign="center">Mifos X is up to date</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Information/Resources Menus -->
|
||||||
<mat-menu #resourcesMenu="matMenu">
|
<mat-menu #resourcesMenu="matMenu">
|
||||||
<a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/52035622/User+Manual" mat-menu-item>User Manual</a>
|
<a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/52035622/User+Manual" target="_blank" mat-menu-item>User Manual</a>
|
||||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Apache+Fineract+1.0+Functional+Specifications" mat-menu-item>Functional Specifications</a>
|
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Apache+Fineract+1.0+Functional+Specifications" target="_blank" mat-menu-item>Functional Specifications</a>
|
||||||
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone" mat-menu-item>Developer Zone</a>
|
<a href="https://cwiki.apache.org/confluence/display/FINERACT/Contributor%27s+Zone" target="_blank" mat-menu-item>Developer Zone</a>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<mat-menu #communityMenu="matMenu">
|
<mat-menu #communityMenu="matMenu">
|
||||||
<a href="https://groups.google.com/forum/#!forum/mifosusers" mat-menu-item>User Group</a>
|
<a href="https://groups.google.com/forum/#!forum/mifosusers" target="_blank" mat-menu-item>User Group</a>
|
||||||
<a href="https://groups.google.com/forum/#!forum/mifosdeveloper" mat-menu-item>Developer Group</a>
|
<a href="https://groups.google.com/forum/#!forum/mifosdeveloper" target="_blank" mat-menu-item>Developer Group</a>
|
||||||
<a href="http://mifos.org/resources/community/communications/#mifos-irc" mat-menu-item>IRC</a>
|
<a href="http://mifos.org/resources/community/communications/#mifos-irc" target="_blank" mat-menu-item>IRC</a>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<mat-menu #contributeMenu="matMenu">
|
<mat-menu #contributeMenu="matMenu">
|
||||||
<a href="https://mifosforge.jira.com/wiki/spaces/MDZ/pages/92012624/Key+Design+Principles" mat-menu-item>Key Design Principles</a>
|
<a href="https://mifosforge.jira.com/wiki/spaces/MDZ/pages/92012624/Key+Design+Principles" target="_blank" mat-menu-item>Key Design Principles</a>
|
||||||
<a href="https://sourceforge.net/projects/mifos/" mat-menu-item>Working with Code</a>
|
<a href="https://sourceforge.net/projects/mifos/" target="_blank" mat-menu-item>Working with Code</a>
|
||||||
<a href="http://mifos.org/donate/" mat-menu-item>Donate</a>
|
<a href="http://mifos.org/donate/" target="_blank" mat-menu-item>Donate</a>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|||||||
@@ -1,100 +1,56 @@
|
|||||||
.cover {
|
.cover {
|
||||||
background: url("../../assets/images/cover_image.jpg") no-repeat center center;
|
background: url("../../assets/images/cover_image.jpg") no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
// opacity: 0.4
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;;
|
z-index: 0;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
}
|
|
||||||
|
|
||||||
.cover::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
opacity: 0.4;
|
opacity: 0.5;
|
||||||
background: linear-gradient(black, black);
|
background: linear-gradient(#000, #000);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0.6;
|
top: 0;
|
||||||
top: 0;
|
left: 0;
|
||||||
left: 0;
|
display: block;
|
||||||
display: block;
|
z-index: 1;
|
||||||
z-index: 1;
|
position: absolute;
|
||||||
position: absolute;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.cover-content {
|
.cover-content {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
margin: 100px;
|
margin: 100px;
|
||||||
color: white;
|
color: #fff;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
p {
|
.cover-title {
|
||||||
a {
|
border-bottom: 2px solid #fff;
|
||||||
text-decoration: underline;
|
margin: 0 0 20px;
|
||||||
color: white;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.cover-description {
|
||||||
color: lightgray;
|
.cover-link {
|
||||||
}
|
text-decoration: underline;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #d3d3d3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-title {
|
|
||||||
border-bottom: 2px solid white;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
box-shadow: 1px 1px 50px black;
|
box-shadow: 1px 1px 50px #000;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
.img-container {
|
||||||
|
max-width: 18rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-selector {
|
|
||||||
max-width: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-container {
|
|
||||||
max-width: 18rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
margin: 10px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @import "../../theme/theme-variables";
|
|
||||||
// @import "~bootstrap/scss/mixins/_breakpoints";
|
|
||||||
|
|
||||||
// .login-container {
|
|
||||||
// position: absolute;
|
|
||||||
// top: 0;
|
|
||||||
// bottom: 0;
|
|
||||||
// left: 0;
|
|
||||||
// right: 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .login-box {
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
// justify-content: center;
|
|
||||||
// align-items: center;
|
|
||||||
// width: 100%;
|
|
||||||
// min-height: 100%;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .ng-invalid.ng-touched:not(form) {
|
|
||||||
// border-left: 4px solid theme-color("danger");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @include media-breakpoint-down(xs) {
|
|
||||||
// .container {
|
|
||||||
// width: 100%;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { CoreModule } from '../core';
|
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
describe('LanguageSelectorComponent', () => {
|
||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
declarations: [ LoginComponent ]
|
||||||
RouterTestingModule,
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
ReactiveFormsModule,
|
|
||||||
CoreModule
|
|
||||||
],
|
|
||||||
declarations: [LoginComponent]
|
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { I18nService } from '../core/i18n.service';
|
/** rxjs Imports */
|
||||||
import { AlertService } from '../core/alert.service';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
/** Custom Services and Models */
|
||||||
|
import { AlertService, Alert } from '../core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login component.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-login',
|
selector: 'mifosx-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
@@ -13,19 +18,25 @@ import { environment } from '../../environments/environment';
|
|||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
version: string = environment.version;
|
/** True if password requires a reset. */
|
||||||
languages = ['en-US', 'fr-FR'];
|
|
||||||
language = this.languages[0];
|
|
||||||
resetPassword = false;
|
resetPassword = false;
|
||||||
|
/** True if user requires two factor authentication. */
|
||||||
twoFactorAuthenticationRequired = false;
|
twoFactorAuthenticationRequired = false;
|
||||||
alert$: any;
|
/** Subscription to alerts. */
|
||||||
|
alert$: Subscription;
|
||||||
|
|
||||||
constructor(private i18nService: I18nService,
|
/**
|
||||||
private alertService: AlertService,
|
* @param {AlertService} alertService Alert Service.
|
||||||
|
* @param {Router} router Router for navigation.
|
||||||
|
*/
|
||||||
|
constructor(private alertService: AlertService,
|
||||||
private router: Router) { }
|
private router: Router) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to alert event of alert service.
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: any) => {
|
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => {
|
||||||
const alertType = alertEvent.type;
|
const alertType = alertEvent.type;
|
||||||
if (alertType === 'Password Expired') {
|
if (alertType === 'Password Expired') {
|
||||||
this.twoFactorAuthenticationRequired = false;
|
this.twoFactorAuthenticationRequired = false;
|
||||||
@@ -41,18 +52,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setLanguage(language: string) {
|
/**
|
||||||
this.i18nService.language = language;
|
* Unsubscribes from alerts.
|
||||||
}
|
*/
|
||||||
|
|
||||||
get currentLanguage(): string {
|
|
||||||
return this.i18nService.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get languages(): string[] {
|
|
||||||
// return this.i18nService.supportedLanguages;
|
|
||||||
// }
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.alert$.unsubscribe();
|
this.alert$.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
|
/** Custom Modules */
|
||||||
|
import { SharedModule } from '../shared';
|
||||||
import { LoginRoutingModule } from './login-routing.module';
|
import { LoginRoutingModule } from './login-routing.module';
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
import { CoreModule } from '../core/core.module';
|
|
||||||
import { LoginFormComponent } from './login-form/login-form.component';
|
|
||||||
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
|
|
||||||
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
|
||||||
|
|
||||||
|
/** Custom Components */
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
import { LoginFormComponent } from './login-form/login-form.component';
|
||||||
|
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
||||||
|
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login Module
|
||||||
|
*
|
||||||
|
* All components related to user authentication should be declared here.
|
||||||
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
SharedModule,
|
||||||
CoreModule,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
TranslateModule,
|
|
||||||
LoginRoutingModule
|
LoginRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginFormComponent,
|
LoginFormComponent,
|
||||||
TwoFactorAuthenticationComponent,
|
ResetPasswordComponent,
|
||||||
ResetPasswordComponent
|
TwoFactorAuthenticationComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LoginModule { }
|
export class LoginModule { }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** Angular Imports */
|
||||||
|
import { FormGroup, ValidatorFn, ValidationErrors } from '@angular/forms';
|
||||||
|
|
||||||
|
/** Validates that the values of password and confirm password are same. */
|
||||||
|
export const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): ValidationErrors | null => {
|
||||||
|
const password = resetPasswordForm.get('password');
|
||||||
|
const confirmPassword = resetPasswordForm.get('repeatPassword');
|
||||||
|
return password && confirmPassword && password.value !== confirmPassword.value ? { 'passwordsDoNotMatch': true } : null;
|
||||||
|
};
|
||||||
@@ -1,31 +1,44 @@
|
|||||||
<p>Please update your password:</p>
|
<p>Please update your password:</p>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<form fxLayout="column" [formGroup]="resetPasswordForm" (ngSubmit)="resetPassword()">
|
|
||||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
<form fxLayout="column" [formGroup]="resetPasswordForm" (ngSubmit)="resetPassword()" id="reset-password-form">
|
||||||
<span matPrefix><mat-icon>lock</mat-icon> </span>
|
|
||||||
<input type="{{ passwordInputType }}" matInput placeholder="Password" formControlName="password">
|
<mat-form-field fxFlexAlign="center" class="reset-password-input">
|
||||||
|
<span matPrefix>
|
||||||
|
<fa-icon icon="lock" size="lg"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<mat-label>Password</mat-label>
|
||||||
|
<input type="{{ passwordInputType }}" matInput required formControlName="password">
|
||||||
<button mat-button *ngIf="resetPasswordForm.controls.password.value && !loading" matSuffix mat-icon-button
|
<button mat-button *ngIf="resetPasswordForm.controls.password.value && !loading" matSuffix mat-icon-button
|
||||||
aria-label="Clear" (mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
(mousedown)="passwordInputType = 'text'" (mouseup)="passwordInputType = 'password'">
|
||||||
<mat-icon *ngIf="passwordInputType === 'password'">visibility</mat-icon>
|
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
|
||||||
<mat-icon *ngIf="passwordInputType === 'text'">visibility_off</mat-icon>
|
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-error *ngIf="resetPasswordForm.controls.password.hasError('required')">
|
<mat-error *ngIf="resetPasswordForm.controls.password.hasError('required')">
|
||||||
Password is <strong>required</strong>
|
Password is <strong>required</strong>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
|
||||||
<span matPrefix><mat-icon>check_circle</mat-icon> </span>
|
<mat-form-field fxFlexAlign="center" class="reset-password-input">
|
||||||
<input type="password" matInput placeholder="Confirm Password" formControlName="repeatPassword">
|
<span matPrefix>
|
||||||
|
<fa-icon icon="check-circle" size="lg"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<mat-label>Confirm Password</mat-label>
|
||||||
|
<input type="password" matInput required formControlName="repeatPassword">
|
||||||
<mat-error *ngIf="resetPasswordForm.controls.repeatPassword.hasError('required')">
|
<mat-error *ngIf="resetPasswordForm.controls.repeatPassword.hasError('required')">
|
||||||
Confirm Password is <strong>required</strong>
|
Confirm Password is <strong>required</strong>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<p class="error" *ngIf="resetPasswordForm.errors?.passwordsDoNotMatch &&
|
|
||||||
(!resetPasswordForm.controls.password.hasError('required') && !resetPasswordForm.controls.repeatPassword.hasError('required'))">
|
<p class="reset-password-error" *ngIf="resetPasswordForm.errors?.passwordsDoNotMatch &&
|
||||||
|
(!resetPasswordForm.controls.password.hasError('required') && !resetPasswordForm.controls.repeatPassword.hasError('required'))">
|
||||||
Passwords <strong>do not match</strong>
|
Passwords <strong>do not match</strong>
|
||||||
</p>
|
</p>
|
||||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!resetPasswordForm.valid">
|
|
||||||
|
<button mat-raised-button color="primary" fxFlexAlign="center" class="reset-password-button" [disabled]="!resetPasswordForm.valid">
|
||||||
Reset Password
|
Reset Password
|
||||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
.textfield {
|
#reset-password-form {
|
||||||
width: 14rem;
|
.reset-password-input {
|
||||||
margin-top: 0.6rem;
|
width: 14rem;
|
||||||
}
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.reset-password-button {
|
||||||
width: 14rem;
|
width: 14rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.reset-password-error {
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-spinner {
|
mat-spinner {
|
||||||
z-index: 4;
|
float: right;
|
||||||
float: right;
|
margin: 0.5rem 0;
|
||||||
margin: 0.5rem 0;
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
/** Angular Imports */
|
||||||
import { FormGroup, FormBuilder, Validators, ValidatorFn, ValidationErrors } from '@angular/forms';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { AuthenticationService } from '../../core';
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): ValidationErrors | null => {
|
/** Custom Services */
|
||||||
const password = resetPasswordForm.controls.password;
|
import { AuthenticationService } from '../../core';
|
||||||
const confirmPassword = resetPasswordForm.controls.repeatPassword;
|
|
||||||
return password && confirmPassword && password.value !== confirmPassword.value ? { 'passwordsDoNotMatch': true } : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/** Custom Validators */
|
||||||
|
import { confirmPasswordValidator } from './confirm-password.validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password component.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-reset-password',
|
selector: 'mifosx-reset-password',
|
||||||
templateUrl: './reset-password.component.html',
|
templateUrl: './reset-password.component.html',
|
||||||
@@ -16,25 +21,33 @@ const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): Va
|
|||||||
})
|
})
|
||||||
export class ResetPasswordComponent implements OnInit {
|
export class ResetPasswordComponent implements OnInit {
|
||||||
|
|
||||||
|
/** Reset password form group. */
|
||||||
resetPasswordForm: FormGroup;
|
resetPasswordForm: FormGroup;
|
||||||
loading = false;
|
/** Password input field type. */
|
||||||
passwordInputType: string;
|
passwordInputType: string;
|
||||||
|
/** True if loading. */
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormBuilder} formBuilder Form Builder.
|
||||||
|
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||||
|
*/
|
||||||
constructor(private formBuilder: FormBuilder,
|
constructor(private formBuilder: FormBuilder,
|
||||||
private authenticationService: AuthenticationService) { }
|
private authenticationService: AuthenticationService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates reset password form.
|
||||||
|
*
|
||||||
|
* Initializes password input field type.
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.createResetPasswordForm();
|
this.createResetPasswordForm();
|
||||||
this.passwordInputType = 'password';
|
this.passwordInputType = 'password';
|
||||||
}
|
}
|
||||||
|
|
||||||
private createResetPasswordForm() {
|
/**
|
||||||
this.resetPasswordForm = this.formBuilder.group({
|
* Resets the password of user.
|
||||||
'password': ['', Validators.required],
|
*/
|
||||||
'repeatPassword': ['', Validators.required]
|
|
||||||
}, { validator: confirmPasswordValidator });
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword() {
|
resetPassword() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.resetPasswordForm.disable();
|
this.resetPasswordForm.disable();
|
||||||
@@ -42,10 +55,20 @@ export class ResetPasswordComponent implements OnInit {
|
|||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.resetPasswordForm.reset();
|
this.resetPasswordForm.reset();
|
||||||
this.resetPasswordForm.markAsPristine();
|
this.resetPasswordForm.markAsPristine();
|
||||||
// Bug: Validation errors won't get removed on reset
|
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||||
this.resetPasswordForm.enable();
|
this.resetPasswordForm.enable();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
})).subscribe();
|
})).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates reset password form.
|
||||||
|
*/
|
||||||
|
private createResetPasswordForm() {
|
||||||
|
this.resetPasswordForm = this.formBuilder.group({
|
||||||
|
'password': ['', Validators.required],
|
||||||
|
'repeatPassword': ['', Validators.required]
|
||||||
|
}, { validator: confirmPasswordValidator });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,60 @@
|
|||||||
<p><strong>Two Factor Authentication</strong></p>
|
<p><strong>Two Factor Authentication</strong></p>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<!-- Select delivery method to receive OTP -->
|
||||||
<p *ngIf="!otpRequested">Please select a delivery method:</p>
|
<p *ngIf="!otpRequested">Please select a delivery method:</p>
|
||||||
<form fxLayout="column" *ngIf="!otpRequested" [formGroup]="twoFactorAuthenticationDeliveryMethodForm" (ngSubmit)="requestOTP()">
|
|
||||||
|
<form fxLayout="column" *ngIf="!otpRequested" [formGroup]="twoFactorAuthenticationDeliveryMethodForm" class="two-factor-auth-form"
|
||||||
|
(ngSubmit)="requestOTP()">
|
||||||
|
|
||||||
<mat-radio-group fxLayout="column" fxFlexAlign="center" formControlName="twoFactorAuthenticationDeliveryMethod">
|
<mat-radio-group fxLayout="column" fxFlexAlign="center" formControlName="twoFactorAuthenticationDeliveryMethod">
|
||||||
<mat-radio-button *ngFor="let twoFactorAuthenticationDeliveryMethod of twoFactorAuthenticationDeliveryMethods"
|
<mat-radio-button *ngFor="let twoFactorAuthenticationDeliveryMethod of twoFactorAuthenticationDeliveryMethods"
|
||||||
[value]="twoFactorAuthenticationDeliveryMethod">
|
[value]="twoFactorAuthenticationDeliveryMethod">
|
||||||
Send {{ twoFactorAuthenticationDeliveryMethod.name }} to {{ twoFactorAuthenticationDeliveryMethod.target }}
|
Send {{ twoFactorAuthenticationDeliveryMethod.name }} to {{ twoFactorAuthenticationDeliveryMethod.target }}
|
||||||
</mat-radio-button>
|
</mat-radio-button>
|
||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
|
|
||||||
<button mat-raised-button color="primary" fxFlexAlign="center" fxFlexFill [disabled]="!twoFactorAuthenticationDeliveryMethodForm.valid">
|
<button mat-raised-button color="primary" fxFlexAlign="center" fxFlexFill [disabled]="!twoFactorAuthenticationDeliveryMethodForm.valid">
|
||||||
Request OTP
|
Request OTP
|
||||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- show input for otp request -->
|
<!-- Show input for OTP -->
|
||||||
<p *ngIf="otpRequested">Please enter the OTP:</p>
|
<p *ngIf="otpRequested">Please enter the OTP:</p>
|
||||||
<form fxLayout="column" *ngIf="otpRequested" [formGroup]="twoFactorAuthenticationForm" (ngSubmit)="validateOTP()">
|
|
||||||
<mat-form-field fxFlexAlign="center" class="textfield">
|
<form fxLayout="column" *ngIf="otpRequested" [formGroup]="twoFactorAuthenticationForm" class="two-factor-auth-form"
|
||||||
<span matPrefix><mat-icon>verified_user</mat-icon> </span>
|
(ngSubmit)="validateOTP()">
|
||||||
<input matInput placeholder="OTP" autocomplete="off" formControlName="otp">
|
|
||||||
<mat-hint align="start"><strong>Delivery Method:</strong> {{ selectedTwoFactorAuthenticationDeliveryMethod.name }} </mat-hint>
|
<mat-form-field fxFlexAlign="center" class="two-factor-auth-input">
|
||||||
<mat-hint align="end"><strong>Validity:</strong> {{ tokenValidityTime / 60 }} mins </mat-hint>
|
<span matPrefix>
|
||||||
|
<fa-icon icon="user-shield"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<mat-label>OTP</mat-label>
|
||||||
|
<input type="password" matInput autocomplete="off" required formControlName="otp">
|
||||||
|
<mat-hint align="start">
|
||||||
|
<strong>Delivery Method:</strong> {{ selectedTwoFactorAuthenticationDeliveryMethod.name }}
|
||||||
|
</mat-hint>
|
||||||
|
<mat-hint align="end">
|
||||||
|
<strong>Validity:</strong> {{ tokenValidityTime / 60 }} mins
|
||||||
|
</mat-hint>
|
||||||
<mat-error *ngIf="twoFactorAuthenticationForm.controls.otp.hasError('required')">
|
<mat-error *ngIf="twoFactorAuthenticationForm.controls.otp.hasError('required')">
|
||||||
OTP is <strong>required</strong>
|
OTP is <strong>required</strong>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="primary" fxFlexAlign="center" class="btn" [disabled]="!twoFactorAuthenticationForm.valid">
|
|
||||||
|
<button mat-raised-button color="primary" fxFlexAlign="center" class="two-factor-auth-button"
|
||||||
|
[disabled]="!twoFactorAuthenticationForm.valid">
|
||||||
Validate OTP
|
Validate OTP
|
||||||
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-button fxFlexAlign="center" class="btn" (click)="resendOTP()" [disabled]="loading || resendOTPLoading">
|
|
||||||
|
<button type="button" mat-button fxFlexAlign="center" class="two-factor-auth-button" (click)="resendOTP()"
|
||||||
|
[disabled]="loading || resendOTPLoading">
|
||||||
Resend OTP
|
Resend OTP
|
||||||
<mat-spinner [diameter]="20" *ngIf="resendOTPLoading"></mat-spinner>
|
<mat-spinner [diameter]="20" *ngIf="resendOTPLoading"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
.btn {
|
.two-factor-auth-form {
|
||||||
width: 14rem;
|
mat-radio-button {
|
||||||
margin-top: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
mat-radio-button {
|
&:last-child {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mat-radio-button:last-child {
|
.two-factor-auth-button {
|
||||||
margin-bottom: 1rem;
|
width: 14rem;
|
||||||
}
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.textfield {
|
.two-factor-auth-input {
|
||||||
width: 14rem;
|
width: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-spinner {
|
mat-spinner {
|
||||||
z-index: 4;
|
float: right;
|
||||||
float: right;
|
margin: 0.5rem 0;
|
||||||
margin: 0.5rem 0;
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
import { AuthenticationService } from '../../core';
|
|
||||||
|
/** rxjs Imports */
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { AuthenticationService } from '../../core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two factor authentication component.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-two-factor-authentication',
|
selector: 'mifosx-two-factor-authentication',
|
||||||
templateUrl: './two-factor-authentication.component.html',
|
templateUrl: './two-factor-authentication.component.html',
|
||||||
@@ -10,32 +18,46 @@ import { finalize } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class TwoFactorAuthenticationComponent implements OnInit {
|
export class TwoFactorAuthenticationComponent implements OnInit {
|
||||||
|
|
||||||
|
/** Available delivery methods to receive OTP. */
|
||||||
twoFactorAuthenticationDeliveryMethods: any;
|
twoFactorAuthenticationDeliveryMethods: any;
|
||||||
|
/** Delivery method selected to receive OTP. */
|
||||||
selectedTwoFactorAuthenticationDeliveryMethod: any;
|
selectedTwoFactorAuthenticationDeliveryMethod: any;
|
||||||
loading = false;
|
/** True if OTP is requested. */
|
||||||
resendOTPLoading = false;
|
|
||||||
otpRequested = false;
|
otpRequested = false;
|
||||||
|
/** Time for which OTP is valid. */
|
||||||
tokenValidityTime: number;
|
tokenValidityTime: number;
|
||||||
|
/** Two factor authentication delivery method form group. */
|
||||||
twoFactorAuthenticationDeliveryMethodForm: FormGroup;
|
twoFactorAuthenticationDeliveryMethodForm: FormGroup;
|
||||||
|
/** Two factor authentication form group. */
|
||||||
twoFactorAuthenticationForm: FormGroup;
|
twoFactorAuthenticationForm: FormGroup;
|
||||||
|
/** True if loading. */
|
||||||
|
loading = false;
|
||||||
|
/** True if loading. */
|
||||||
|
resendOTPLoading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FormBuilder} formBuilder Form Builder.
|
||||||
|
* @param {AuthenticationService} authenticationService Authentication Service.
|
||||||
|
*/
|
||||||
constructor(private formBuilder: FormBuilder,
|
constructor(private formBuilder: FormBuilder,
|
||||||
private authenticationService: AuthenticationService) { }
|
private authenticationService: AuthenticationService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates two factor authentication delivery method form.
|
||||||
|
*
|
||||||
|
* Gets the delivery methods available to receive OTP.
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.createTwoFactorAuthenticationDeliveryMethodForm();
|
this.createTwoFactorAuthenticationDeliveryMethodForm();
|
||||||
this.authenticationService.getDeliveryMethods()
|
this.authenticationService.getDeliveryMethods()
|
||||||
.subscribe(deliveryMethods => {
|
.subscribe((deliveryMethods: any) => {
|
||||||
this.twoFactorAuthenticationDeliveryMethods = deliveryMethods;
|
this.twoFactorAuthenticationDeliveryMethods = deliveryMethods;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTwoFactorAuthenticationDeliveryMethodForm() {
|
/**
|
||||||
this.twoFactorAuthenticationDeliveryMethodForm = this.formBuilder.group({
|
* Requests OTP via the selected delivery method.
|
||||||
'twoFactorAuthenticationDeliveryMethod': ['', Validators.required]
|
*/
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
requestOTP() {
|
requestOTP() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.twoFactorAuthenticationDeliveryMethodForm.disable();
|
this.twoFactorAuthenticationDeliveryMethodForm.disable();
|
||||||
@@ -46,7 +68,7 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
|||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.twoFactorAuthenticationDeliveryMethodForm.reset();
|
this.twoFactorAuthenticationDeliveryMethodForm.reset();
|
||||||
this.twoFactorAuthenticationDeliveryMethodForm.markAsPristine();
|
this.twoFactorAuthenticationDeliveryMethodForm.markAsPristine();
|
||||||
// Bug: Validation errors won't get removed on reset
|
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||||
this.twoFactorAuthenticationDeliveryMethodForm.enable();
|
this.twoFactorAuthenticationDeliveryMethodForm.enable();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}))
|
}))
|
||||||
@@ -57,12 +79,9 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTwoFactorAuthenticationForm() {
|
/**
|
||||||
this.twoFactorAuthenticationForm = this.formBuilder.group({
|
* Validates the OTP and authenticates the user.
|
||||||
'otp': ['', Validators.required]
|
*/
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
validateOTP() {
|
validateOTP() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.twoFactorAuthenticationForm.disable();
|
this.twoFactorAuthenticationForm.disable();
|
||||||
@@ -70,12 +89,15 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
|||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.twoFactorAuthenticationForm.reset();
|
this.twoFactorAuthenticationForm.reset();
|
||||||
this.twoFactorAuthenticationForm.markAsPristine();
|
this.twoFactorAuthenticationForm.markAsPristine();
|
||||||
// Bug: Validation errors won't get removed on reset
|
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||||
this.twoFactorAuthenticationForm.enable();
|
this.twoFactorAuthenticationForm.enable();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
})).subscribe();
|
})).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends OTP via the selected delivery method.
|
||||||
|
*/
|
||||||
resendOTP() {
|
resendOTP() {
|
||||||
this.resendOTPLoading = true;
|
this.resendOTPLoading = true;
|
||||||
this.twoFactorAuthenticationForm.disable();
|
this.twoFactorAuthenticationForm.disable();
|
||||||
@@ -83,9 +105,28 @@ export class TwoFactorAuthenticationComponent implements OnInit {
|
|||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.twoFactorAuthenticationForm.reset();
|
this.twoFactorAuthenticationForm.reset();
|
||||||
this.twoFactorAuthenticationForm.markAsPristine();
|
this.twoFactorAuthenticationForm.markAsPristine();
|
||||||
// Bug: Validation errors won't get removed on reset
|
// Angular Material Bug: Validation errors won't get removed on reset.
|
||||||
this.twoFactorAuthenticationForm.enable();
|
this.twoFactorAuthenticationForm.enable();
|
||||||
this.resendOTPLoading = false;
|
this.resendOTPLoading = false;
|
||||||
})).subscribe();
|
})).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates two factor authentication delivery method form.
|
||||||
|
*/
|
||||||
|
private createTwoFactorAuthenticationDeliveryMethodForm() {
|
||||||
|
this.twoFactorAuthenticationDeliveryMethodForm = this.formBuilder.group({
|
||||||
|
'twoFactorAuthenticationDeliveryMethod': ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates two factor authentication form.
|
||||||
|
*/
|
||||||
|
private createTwoFactorAuthenticationForm() {
|
||||||
|
this.twoFactorAuthenticationForm = this.formBuilder.group({
|
||||||
|
'otp': ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
|||||||
export class DeleteDialogComponent implements OnInit {
|
export class DeleteDialogComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param dialogRef Component reference to dialog.
|
* @param {MatDialogRef} dialogRef Component reference to dialog.
|
||||||
* @param data Provides a deleteContext.
|
* @param {any} data Provides a deleteContext.
|
||||||
*/
|
*/
|
||||||
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>,
|
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class FileUploadComponent implements OnInit {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file.
|
* Uploads a file.
|
||||||
* @param {any} $event The file input change event.
|
* @param {any} event The file input change event.
|
||||||
*/
|
*/
|
||||||
onFileSelected($event: any) {
|
onFileSelected($event: any) {
|
||||||
this.fileName = $event.target.files[0].name;
|
this.fileName = $event.target.files[0].name;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div fxLayout="column" id="footer" class="m-b-20">
|
||||||
|
<mat-divider fxFlexAlign="center" class="divider"></mat-divider>
|
||||||
|
<p fxFlexAlign="center" class="footer-content">Version {{ version }}</p>
|
||||||
|
<p fxFlexAlign="center" class="footer-content">Mifos X is up to date</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#footer {
|
||||||
|
.divider {
|
||||||
|
margin: 1rem 1rem 0.5rem;
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FooterComponent } from './footer.component';
|
||||||
|
|
||||||
|
describe('FooterComponent', () => {
|
||||||
|
let component: FooterComponent;
|
||||||
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ FooterComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FooterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/** Angular Imports */
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
/** Environment Configuration */
|
||||||
|
import { environment } from 'environments/environment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Footer component.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-footer',
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
styleUrls: ['./footer.component.scss']
|
||||||
|
})
|
||||||
|
export class FooterComponent implements OnInit {
|
||||||
|
|
||||||
|
/** Mifos X version. */
|
||||||
|
version: string = environment.version;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,18 +5,31 @@ 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 {
|
||||||
|
faCheckCircle,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faFileUpload,
|
faFileUpload,
|
||||||
faSearch
|
faLock,
|
||||||
|
faSearch,
|
||||||
|
faUserCircle,
|
||||||
|
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(
|
||||||
|
faCheckCircle,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faFileUpload,
|
faFileUpload,
|
||||||
faSearch
|
faLock,
|
||||||
|
faSearch,
|
||||||
|
faUserCircle,
|
||||||
|
faUserShield
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icons Module
|
* Icons Module
|
||||||
|
*
|
||||||
* Angular Font Awesome module is exported here.
|
* Angular Font Awesome module is exported here.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './shared.module';
|
export { SharedModule } from './shared.module';
|
||||||
export { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
export { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<mat-form-field id="language-selector">
|
||||||
|
<mat-label>Language</mat-label>
|
||||||
|
<mat-select [formControl]="languageSelector">
|
||||||
|
<mat-option *ngFor="let language of languages" [value]="language">
|
||||||
|
{{ language }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#language-selector {
|
||||||
|
max-width: 4rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LanguageSelectorComponent } from './language-selector.component';
|
||||||
|
|
||||||
|
describe('LanguageSelectorComponent', () => {
|
||||||
|
let component: LanguageSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<LanguageSelectorComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ LanguageSelectorComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LanguageSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/** Angular Imports */
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { I18nService } from 'app/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language selector component.
|
||||||
|
*
|
||||||
|
* TODO: Decision to be taken on using ngx-translate or angular-internationalization
|
||||||
|
* to provide language support in the application.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-language-selector',
|
||||||
|
templateUrl: './language-selector.component.html',
|
||||||
|
styleUrls: ['./language-selector.component.scss']
|
||||||
|
})
|
||||||
|
export class LanguageSelectorComponent implements OnInit {
|
||||||
|
|
||||||
|
/** Language selector form control. */
|
||||||
|
languageSelector = new FormControl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the language of the application in the selector on initial setup.
|
||||||
|
* @param {I18nService} i18nService Internationalization Service.
|
||||||
|
*/
|
||||||
|
constructor(private i18nService: I18nService) {
|
||||||
|
this.languageSelector.setValue(this.currentLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new language to be used by the application.
|
||||||
|
* @param {string} language New language.
|
||||||
|
*/
|
||||||
|
setLanguage(language: string) {
|
||||||
|
this.i18nService.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current language used by the application.
|
||||||
|
* @returns {string} Current language.
|
||||||
|
*/
|
||||||
|
get currentLanguage(): string {
|
||||||
|
return this.i18nService.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the languages supported by the application.
|
||||||
|
* @return {string[]} Supported languages.
|
||||||
|
*/
|
||||||
|
get languages(): string[] {
|
||||||
|
return this.i18nService.supportedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Material Module
|
* Material Module
|
||||||
|
*
|
||||||
* Angular CDK, Angular Material and Flex Layout modules are exported here.
|
* Angular CDK, Angular Material and Flex Layout modules are exported here.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { FileUploadComponent } from './file-upload/file-upload.component';
|
|
||||||
import { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
import { DeleteDialogComponent } from './delete-dialog/delete-dialog.component';
|
||||||
|
import { FileUploadComponent } from './file-upload/file-upload.component';
|
||||||
|
import { FooterComponent } from './footer/footer.component';
|
||||||
|
import { LanguageSelectorComponent } from './language-selector/language-selector.component';
|
||||||
|
|
||||||
/** Custom Modules */
|
/** Custom Modules */
|
||||||
import { MaterialModule } from './material.module';
|
|
||||||
import { IconsModule } from './icons.module';
|
import { IconsModule } from './icons.module';
|
||||||
|
import { MaterialModule } from './material.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared Module
|
* Shared Module
|
||||||
|
*
|
||||||
* Modules and components that are shared throughout the application should be here.
|
* Modules and components that are shared throughout the application should be here.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IconsModule,
|
IconsModule,
|
||||||
MaterialModule
|
MaterialModule,
|
||||||
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
DeleteDialogComponent,
|
DeleteDialogComponent,
|
||||||
FileUploadComponent
|
FileUploadComponent,
|
||||||
|
FooterComponent,
|
||||||
|
LanguageSelectorComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FileUploadComponent,
|
FileUploadComponent,
|
||||||
|
FooterComponent,
|
||||||
|
LanguageSelectorComponent,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
IconsModule,
|
IconsModule,
|
||||||
MaterialModule
|
MaterialModule,
|
||||||
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
DeleteDialogComponent
|
DeleteDialogComponent
|
||||||
|
|||||||
Reference in New Issue
Block a user