diff --git a/.stylelintrc b/.stylelintrc index 4eee4c79c..f506c6c67 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -10,9 +10,9 @@ ], "selector-attribute-quotes": "always", "string-quotes": "double", - "max-nesting-depth": 3, - "selector-max-compound-selectors": 3, - "selector-max-specificity": "0,3,2", + "max-nesting-depth": 5, + "selector-max-compound-selectors": 5, + "selector-max-specificity": "1,3,2", "declaration-no-important": true, "at-rule-no-vendor-prefix": true, "media-feature-name-no-vendor-prefix": true, @@ -22,9 +22,18 @@ "no-empty-source": null, "selector-class-pattern": "[a-z-]+", "selector-id-pattern": "[a-z-]+", - "selector-max-id": 0, + "selector-max-id": 1, "selector-no-qualifying-type": true, "selector-max-universal": 0, + "selector-type-no-unknown": [ + true, + { + "ignoreTypes": [ + "mat-spinner", + "mat-radio-button" + ] + } + ], "selector-pseudo-element-no-unknown": [ true, { diff --git a/src/app/core/alert.service.ts b/src/app/core/alert.service.ts index 4d2a597ec..029935d0a 100644 --- a/src/app/core/alert.service.ts +++ b/src/app/core/alert.service.ts @@ -1,17 +1,22 @@ import { Injectable, EventEmitter } from '@angular/core'; +export interface Alert { + type: string; + message: string; +} + @Injectable({ providedIn: 'root' }) export class AlertService { - public alertEvent: EventEmitter; + public alertEvent: EventEmitter; constructor() { this.alertEvent = new EventEmitter(); } - alert(alertEvent: any) { + alert(alertEvent: Alert) { this.alertEvent.emit(alertEvent); } diff --git a/src/app/core/index.ts b/src/app/core/index.ts index 096789040..ee68fd0be 100644 --- a/src/app/core/index.ts +++ b/src/app/core/index.ts @@ -11,3 +11,4 @@ export * from './http/error-handler.interceptor'; export * from './route.service'; export * from './route-reusable-strategy'; export * from './logger.service'; +export * from './alert.service'; diff --git a/src/app/login/login-form/login-form.component.html b/src/app/login/login-form/login-form.component.html index 4382526af..7894dc357 100644 --- a/src/app/login/login-form/login-form.component.html +++ b/src/app/login/login-form/login-form.component.html @@ -1,27 +1,41 @@ -
- - account_circle   - + + + - - lock   - + + + Remember me - - + + + diff --git a/src/app/login/login-form/login-form.component.scss b/src/app/login/login-form/login-form.component.scss index 87b95bf57..f4770acca 100644 --- a/src/app/login/login-form/login-form.component.scss +++ b/src/app/login/login-form/login-form.component.scss @@ -1,15 +1,16 @@ -.textfield { - width: 14rem; - margin-bottom: 0.6rem; -} +#login-form { + .login-input { + width: 14rem; + margin-bottom: 0.6rem; + } -.btn { - width: 14rem; - margin-top: 1rem; -} + .login-button { + width: 14rem; + margin-top: 1rem; + } -mat-spinner { - z-index: 4; - float: right; - margin: 0.5rem 0; + mat-spinner { + float: right; + margin: 0.5rem 0; + } } diff --git a/src/app/login/login-form/login-form.component.ts b/src/app/login/login-form/login-form.component.ts index ecd520919..57809a608 100644 --- a/src/app/login/login-form/login-form.component.ts +++ b/src/app/login/login-form/login-form.component.ts @@ -1,12 +1,16 @@ +/** Angular Imports */ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +/** rxjs Imports */ import { finalize } from 'rxjs/operators'; -import { Logger, AuthenticationService } from '../../core'; - -const log = new Logger('Login'); +/** Custom Services */ +import { AuthenticationService } from '../../core'; +/** + * Login form component. + */ @Component({ selector: 'mifosx-login-form', templateUrl: './login-form.component.html', @@ -14,37 +18,56 @@ const log = new Logger('Login'); }) export class LoginFormComponent implements OnInit { + /** Login form group. */ loginForm: FormGroup; + /** Password input field type. */ passwordInputType: string; - error: string; + /** True if loading. */ loading = false; - authenticationEvent: any; + /** + * @param {FormBuilder} formBuilder Form Builder. + * @param {AuthenticationService} authenticationService Authentication Service. + */ constructor(private formBuilder: FormBuilder, private authenticationService: AuthenticationService) { } + /** + * Creates login form. + * + * Initializes password input field type. + */ ngOnInit() { this.createLoginForm(); this.passwordInputType = 'password'; } - public login() { + /** + * Authenticates the user if the credentials are valid. + */ + login() { this.loading = true; this.loginForm.disable(); this.authenticationService.login(this.loginForm.value) .pipe(finalize(() => { this.loginForm.reset(); 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.loading = false; })).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() { this.loginForm = this.formBuilder.group({ 'username': ['', Validators.required], diff --git a/src/app/login/login-routing.module.ts b/src/app/login/login-routing.module.ts index 0581e3fc0..47654f7ca 100644 --- a/src/app/login/login-routing.module.ts +++ b/src/app/login/login-routing.module.ts @@ -1,9 +1,14 @@ +/** Angular Imports */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +/** Transalation Imports */ import { extract } from '../core'; + +/** Custom Components */ import { LoginComponent } from './login.component'; +/** Login Routes */ const routes: Routes = [ { path: 'login', @@ -12,6 +17,11 @@ const routes: Routes = [ } ]; +/** + * Login Routing Module + * + * Configures the login routes. + */ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 7c463609e..14377a1e0 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -1,78 +1,88 @@ +
- - - +

Mifos

-

- Mifos X is designed by the Mifos Initiative. A global community 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? Get involved! +

+ Mifos X is designed by the Mifos Initiative. A global community 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? Get involved!

- +
+ - User Manual - Functional Specifications - Developer Zone + User Manual + Functional Specifications + Developer Zone - User Group - Developer Group - IRC + User Group + Developer Group + IRC - Key Design Principles - Working with Code - Donate + Key Design Principles + Working with Code + Donate diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index 5c785bd03..58e892abe 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -1,100 +1,56 @@ .cover { background: url("../../assets/images/cover_image.jpg") no-repeat center center; background-size: cover; - // opacity: 0.4 position: relative; - z-index: 0;; + z-index: 0; max-width: 70vw; -} -.cover::after { - content: ''; - opacity: 0.4; - background: linear-gradient(black, black); - width: 100%; - height: 100%; - opacity: 0.6; - top: 0; - left: 0; - display: block; - z-index: 1; - position: absolute; -} + &::after { + content: ""; + opacity: 0.5; + background: linear-gradient(#000, #000); + width: 100%; + height: 100%; + top: 0; + left: 0; + display: block; + z-index: 1; + position: absolute; + } -.cover-content { - display: block; - max-width: 80%; - margin: 100px; - color: white; - text-align: justify; - z-index: 2; + .cover-content { + display: block; + max-width: 80%; + margin: 100px; + color: #fff; + text-align: justify; + z-index: 2; - p { - a { - text-decoration: underline; - color: white; - } + .cover-title { + border-bottom: 2px solid #fff; + margin: 0 0 20px; + padding-bottom: 10px; + } - a:hover { - color: lightgray; - } + .cover-description { + .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 { - box-shadow: 1px 1px 50px black; + box-shadow: 1px 1px 50px #000; height: 100vh; 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%; -// } -// } diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts index 0061e07e8..797757697 100644 --- a/src/app/login/login.component.spec.ts +++ b/src/app/login/login.component.spec.ts @@ -1,25 +1,14 @@ 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'; -describe('LoginComponent', () => { +describe('LanguageSelectorComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - TranslateModule.forRoot(), - ReactiveFormsModule, - CoreModule - ], - declarations: [LoginComponent] + declarations: [ LoginComponent ] }) .compileComponents(); })); diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 75dd37fd3..37ad7155e 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,11 +1,16 @@ +/** Angular Imports */ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; -import { I18nService } from '../core/i18n.service'; -import { AlertService } from '../core/alert.service'; +/** rxjs Imports */ +import { Subscription } from 'rxjs'; -import { environment } from '../../environments/environment'; +/** Custom Services and Models */ +import { AlertService, Alert } from '../core'; +/** + * Login component. + */ @Component({ selector: 'mifosx-login', templateUrl: './login.component.html', @@ -13,19 +18,25 @@ import { environment } from '../../environments/environment'; }) export class LoginComponent implements OnInit, OnDestroy { - version: string = environment.version; - languages = ['en-US', 'fr-FR']; - language = this.languages[0]; + /** True if password requires a reset. */ resetPassword = false; + /** True if user requires two factor authentication. */ 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) { } + /** + * Subscribes to alert event of alert service. + */ ngOnInit() { - this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: any) => { + this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => { const alertType = alertEvent.type; if (alertType === 'Password Expired') { this.twoFactorAuthenticationRequired = false; @@ -41,18 +52,9 @@ export class LoginComponent implements OnInit, OnDestroy { }); } - setLanguage(language: string) { - this.i18nService.language = language; - } - - get currentLanguage(): string { - return this.i18nService.language; - } - - // get languages(): string[] { - // return this.i18nService.supportedLanguages; - // } - + /** + * Unsubscribes from alerts. + */ ngOnDestroy() { this.alert$.unsubscribe(); } diff --git a/src/app/login/login.module.ts b/src/app/login/login.module.ts index 32174a7ba..644893078 100644 --- a/src/app/login/login.module.ts +++ b/src/app/login/login.module.ts @@ -1,30 +1,31 @@ +/** Angular Imports */ 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 { 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({ imports: [ - CommonModule, - CoreModule, - FormsModule, - ReactiveFormsModule, - TranslateModule, + SharedModule, LoginRoutingModule ], declarations: [ LoginComponent, LoginFormComponent, - TwoFactorAuthenticationComponent, - ResetPasswordComponent + ResetPasswordComponent, + TwoFactorAuthenticationComponent ] }) export class LoginModule { } diff --git a/src/app/login/reset-password/confirm-password.validator.ts b/src/app/login/reset-password/confirm-password.validator.ts new file mode 100644 index 000000000..b98b27022 --- /dev/null +++ b/src/app/login/reset-password/confirm-password.validator.ts @@ -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; +}; diff --git a/src/app/login/reset-password/reset-password.component.html b/src/app/login/reset-password/reset-password.component.html index 4b66dcaba..3227d743c 100644 --- a/src/app/login/reset-password/reset-password.component.html +++ b/src/app/login/reset-password/reset-password.component.html @@ -1,31 +1,44 @@

Please update your password:

+ -
- - lock   - + + + + + +    + + Password + Password is required - - check_circle   - + + + +    + + Confirm Password + Confirm Password is required -

+ +

Passwords do not match

- + diff --git a/src/app/login/reset-password/reset-password.component.scss b/src/app/login/reset-password/reset-password.component.scss index 69af0b832..d9ca19a0c 100644 --- a/src/app/login/reset-password/reset-password.component.scss +++ b/src/app/login/reset-password/reset-password.component.scss @@ -1,21 +1,22 @@ -.textfield { - width: 14rem; - margin-top: 0.6rem; -} +#reset-password-form { + .reset-password-input { + width: 14rem; + margin-top: 0.6rem; + } -.btn { - width: 14rem; - margin-top: 1rem; -} + .reset-password-button { + width: 14rem; + margin-top: 1rem; + } -.error { - color: #f44336; - font-size: 80%; - margin: 0; -} + .reset-password-error { + color: #f44336; + font-size: 80%; + margin: 0; + } -mat-spinner { - z-index: 4; - float: right; - margin: 0.5rem 0; + mat-spinner { + float: right; + margin: 0.5rem 0; + } } diff --git a/src/app/login/reset-password/reset-password.component.ts b/src/app/login/reset-password/reset-password.component.ts index 20f6b9dd5..22190eed3 100644 --- a/src/app/login/reset-password/reset-password.component.ts +++ b/src/app/login/reset-password/reset-password.component.ts @@ -1,14 +1,19 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { FormGroup, FormBuilder, Validators, ValidatorFn, ValidationErrors } from '@angular/forms'; -import { AuthenticationService } from '../../core'; +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; + +/** rxjs Imports */ import { finalize } from 'rxjs/operators'; -const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): ValidationErrors | null => { - const password = resetPasswordForm.controls.password; - const confirmPassword = resetPasswordForm.controls.repeatPassword; - return password && confirmPassword && password.value !== confirmPassword.value ? { 'passwordsDoNotMatch': true } : null; -}; +/** Custom Services */ +import { AuthenticationService } from '../../core'; +/** Custom Validators */ +import { confirmPasswordValidator } from './confirm-password.validator'; + +/** + * Reset password component. + */ @Component({ selector: 'mifosx-reset-password', templateUrl: './reset-password.component.html', @@ -16,25 +21,33 @@ const confirmPasswordValidator: ValidatorFn = (resetPasswordForm: FormGroup): Va }) export class ResetPasswordComponent implements OnInit { + /** Reset password form group. */ resetPasswordForm: FormGroup; - loading = false; + /** Password input field type. */ passwordInputType: string; + /** True if loading. */ + loading = false; + /** + * @param {FormBuilder} formBuilder Form Builder. + * @param {AuthenticationService} authenticationService Authentication Service. + */ constructor(private formBuilder: FormBuilder, private authenticationService: AuthenticationService) { } + /** + * Creates reset password form. + * + * Initializes password input field type. + */ ngOnInit() { this.createResetPasswordForm(); this.passwordInputType = 'password'; } - private createResetPasswordForm() { - this.resetPasswordForm = this.formBuilder.group({ - 'password': ['', Validators.required], - 'repeatPassword': ['', Validators.required] - }, { validator: confirmPasswordValidator }); - } - + /** + * Resets the password of user. + */ resetPassword() { this.loading = true; this.resetPasswordForm.disable(); @@ -42,10 +55,20 @@ export class ResetPasswordComponent implements OnInit { .pipe(finalize(() => { this.resetPasswordForm.reset(); 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.loading = false; })).subscribe(); } + /** + * Creates reset password form. + */ + private createResetPasswordForm() { + this.resetPasswordForm = this.formBuilder.group({ + 'password': ['', Validators.required], + 'repeatPassword': ['', Validators.required] + }, { validator: confirmPasswordValidator }); + } + } diff --git a/src/app/login/two-factor-authentication/two-factor-authentication.component.html b/src/app/login/two-factor-authentication/two-factor-authentication.component.html index 7714858c8..fa8f794d7 100644 --- a/src/app/login/two-factor-authentication/two-factor-authentication.component.html +++ b/src/app/login/two-factor-authentication/two-factor-authentication.component.html @@ -1,37 +1,60 @@

Two Factor Authentication

+ + +

Please select a delivery method:

-
+ + + Send {{ twoFactorAuthenticationDeliveryMethod.name }} to {{ twoFactorAuthenticationDeliveryMethod.target }} + +
- +

Please enter the OTP:

-
- - verified_user   - - Delivery Method: {{ selectedTwoFactorAuthenticationDeliveryMethod.name }} - Validity: {{ tokenValidityTime / 60 }} mins + + + + + +    + + OTP + + + Delivery Method: {{ selectedTwoFactorAuthenticationDeliveryMethod.name }} + + + Validity: {{ tokenValidityTime / 60 }} mins + OTP is required - - + diff --git a/src/app/login/two-factor-authentication/two-factor-authentication.component.scss b/src/app/login/two-factor-authentication/two-factor-authentication.component.scss index f4edf2a77..a4aaa687b 100644 --- a/src/app/login/two-factor-authentication/two-factor-authentication.component.scss +++ b/src/app/login/two-factor-authentication/two-factor-authentication.component.scss @@ -1,22 +1,23 @@ -.btn { - width: 14rem; - margin-top: 0.5rem; -} +.two-factor-auth-form { + mat-radio-button { + margin-bottom: 0.5rem; -mat-radio-button { - margin-bottom: 0.5rem; -} + &:last-child { + margin-bottom: 1rem; + } + } -mat-radio-button:last-child { - margin-bottom: 1rem; -} + .two-factor-auth-button { + width: 14rem; + margin-top: 0.5rem; + } -.textfield { - width: 14rem; -} + .two-factor-auth-input { + width: 14rem; + } -mat-spinner { - z-index: 4; - float: right; - margin: 0.5rem 0; + mat-spinner { + float: right; + margin: 0.5rem 0; + } } diff --git a/src/app/login/two-factor-authentication/two-factor-authentication.component.ts b/src/app/login/two-factor-authentication/two-factor-authentication.component.ts index 82d221ab4..0a9e60cf4 100644 --- a/src/app/login/two-factor-authentication/two-factor-authentication.component.ts +++ b/src/app/login/two-factor-authentication/two-factor-authentication.component.ts @@ -1,8 +1,16 @@ +/** Angular Imports */ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { AuthenticationService } from '../../core'; + +/** rxjs Imports */ import { finalize } from 'rxjs/operators'; +/** Custom Services */ +import { AuthenticationService } from '../../core'; + +/** + * Two factor authentication component. + */ @Component({ selector: 'mifosx-two-factor-authentication', templateUrl: './two-factor-authentication.component.html', @@ -10,32 +18,46 @@ import { finalize } from 'rxjs/operators'; }) export class TwoFactorAuthenticationComponent implements OnInit { + /** Available delivery methods to receive OTP. */ twoFactorAuthenticationDeliveryMethods: any; + /** Delivery method selected to receive OTP. */ selectedTwoFactorAuthenticationDeliveryMethod: any; - loading = false; - resendOTPLoading = false; + /** True if OTP is requested. */ otpRequested = false; + /** Time for which OTP is valid. */ tokenValidityTime: number; + /** Two factor authentication delivery method form group. */ twoFactorAuthenticationDeliveryMethodForm: FormGroup; + /** Two factor authentication form group. */ 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, private authenticationService: AuthenticationService) { } + /** + * Creates two factor authentication delivery method form. + * + * Gets the delivery methods available to receive OTP. + */ ngOnInit() { this.createTwoFactorAuthenticationDeliveryMethodForm(); this.authenticationService.getDeliveryMethods() - .subscribe(deliveryMethods => { + .subscribe((deliveryMethods: any) => { this.twoFactorAuthenticationDeliveryMethods = deliveryMethods; }); } - private createTwoFactorAuthenticationDeliveryMethodForm() { - this.twoFactorAuthenticationDeliveryMethodForm = this.formBuilder.group({ - 'twoFactorAuthenticationDeliveryMethod': ['', Validators.required] - }); - } - + /** + * Requests OTP via the selected delivery method. + */ requestOTP() { this.loading = true; this.twoFactorAuthenticationDeliveryMethodForm.disable(); @@ -46,7 +68,7 @@ export class TwoFactorAuthenticationComponent implements OnInit { .pipe(finalize(() => { this.twoFactorAuthenticationDeliveryMethodForm.reset(); 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.loading = false; })) @@ -57,12 +79,9 @@ export class TwoFactorAuthenticationComponent implements OnInit { }); } - private createTwoFactorAuthenticationForm() { - this.twoFactorAuthenticationForm = this.formBuilder.group({ - 'otp': ['', Validators.required] - }); - } - + /** + * Validates the OTP and authenticates the user. + */ validateOTP() { this.loading = true; this.twoFactorAuthenticationForm.disable(); @@ -70,12 +89,15 @@ export class TwoFactorAuthenticationComponent implements OnInit { .pipe(finalize(() => { this.twoFactorAuthenticationForm.reset(); 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.loading = false; })).subscribe(); } + /** + * Resends OTP via the selected delivery method. + */ resendOTP() { this.resendOTPLoading = true; this.twoFactorAuthenticationForm.disable(); @@ -83,9 +105,28 @@ export class TwoFactorAuthenticationComponent implements OnInit { .pipe(finalize(() => { this.twoFactorAuthenticationForm.reset(); 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.resendOTPLoading = false; })).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] + }); + } + } diff --git a/src/app/shared/delete-dialog/delete-dialog.component.ts b/src/app/shared/delete-dialog/delete-dialog.component.ts index 5f4ef5502..76c539ff4 100644 --- a/src/app/shared/delete-dialog/delete-dialog.component.ts +++ b/src/app/shared/delete-dialog/delete-dialog.component.ts @@ -13,8 +13,8 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; export class DeleteDialogComponent implements OnInit { /** - * @param dialogRef Component reference to dialog. - * @param data Provides a deleteContext. + * @param {MatDialogRef} dialogRef Component reference to dialog. + * @param {any} data Provides a deleteContext. */ constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { } diff --git a/src/app/shared/file-upload/file-upload.component.ts b/src/app/shared/file-upload/file-upload.component.ts index f8e388376..c6c807b9b 100644 --- a/src/app/shared/file-upload/file-upload.component.ts +++ b/src/app/shared/file-upload/file-upload.component.ts @@ -21,7 +21,7 @@ export class FileUploadComponent implements OnInit { /** * Uploads a file. - * @param {any} $event The file input change event. + * @param {any} event The file input change event. */ onFileSelected($event: any) { this.fileName = $event.target.files[0].name; diff --git a/src/app/shared/footer/footer.component.html b/src/app/shared/footer/footer.component.html new file mode 100644 index 000000000..29eba9b2e --- /dev/null +++ b/src/app/shared/footer/footer.component.html @@ -0,0 +1,5 @@ + diff --git a/src/app/shared/footer/footer.component.scss b/src/app/shared/footer/footer.component.scss new file mode 100644 index 000000000..d741ce9e6 --- /dev/null +++ b/src/app/shared/footer/footer.component.scss @@ -0,0 +1,10 @@ +#footer { + .divider { + margin: 1rem 1rem 0.5rem; + width: 4rem; + } + + .footer-content { + margin: 0; + } +} diff --git a/src/app/shared/footer/footer.component.spec.ts b/src/app/shared/footer/footer.component.spec.ts new file mode 100644 index 000000000..2ca6c4543 --- /dev/null +++ b/src/app/shared/footer/footer.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FooterComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/footer/footer.component.ts b/src/app/shared/footer/footer.component.ts new file mode 100644 index 000000000..ea6fa7501 --- /dev/null +++ b/src/app/shared/footer/footer.component.ts @@ -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() { + } + +} diff --git a/src/app/shared/icons.module.ts b/src/app/shared/icons.module.ts index 99045f6d3..9b013bd63 100644 --- a/src/app/shared/icons.module.ts +++ b/src/app/shared/icons.module.ts @@ -5,18 +5,31 @@ import { NgModule } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; import { + faCheckCircle, + faEye, + faEyeSlash, faFileUpload, - faSearch + faLock, + faSearch, + faUserCircle, + faUserShield } from '@fortawesome/free-solid-svg-icons'; /** Add icons to the library for convenient access in other components. */ library.add( + faCheckCircle, + faEye, + faEyeSlash, faFileUpload, - faSearch + faLock, + faSearch, + faUserCircle, + faUserShield ); /** * Icons Module + * * Angular Font Awesome module is exported here. */ @NgModule({ diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts index 436ae53e4..fd2a53c9c 100644 --- a/src/app/shared/index.ts +++ b/src/app/shared/index.ts @@ -1,2 +1,2 @@ -export * from './shared.module'; +export { SharedModule } from './shared.module'; export { DeleteDialogComponent } from './delete-dialog/delete-dialog.component'; diff --git a/src/app/shared/language-selector/language-selector.component.html b/src/app/shared/language-selector/language-selector.component.html new file mode 100644 index 000000000..ec3e6041a --- /dev/null +++ b/src/app/shared/language-selector/language-selector.component.html @@ -0,0 +1,8 @@ + + Language + + + {{ language }} + + + diff --git a/src/app/shared/language-selector/language-selector.component.scss b/src/app/shared/language-selector/language-selector.component.scss new file mode 100644 index 000000000..85c1ac9b9 --- /dev/null +++ b/src/app/shared/language-selector/language-selector.component.scss @@ -0,0 +1,3 @@ +#language-selector { + max-width: 4rem; +} diff --git a/src/app/shared/language-selector/language-selector.component.spec.ts b/src/app/shared/language-selector/language-selector.component.spec.ts new file mode 100644 index 000000000..097465729 --- /dev/null +++ b/src/app/shared/language-selector/language-selector.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LanguageSelectorComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LanguageSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/language-selector/language-selector.component.ts b/src/app/shared/language-selector/language-selector.component.ts new file mode 100644 index 000000000..379c73262 --- /dev/null +++ b/src/app/shared/language-selector/language-selector.component.ts @@ -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; + } + +} diff --git a/src/app/shared/material.module.ts b/src/app/shared/material.module.ts index 7eb1e17d2..d77e1e113 100644 --- a/src/app/shared/material.module.ts +++ b/src/app/shared/material.module.ts @@ -41,6 +41,7 @@ import { /** * Material Module + * * Angular CDK, Angular Material and Flex Layout modules are exported here. */ @NgModule({ diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d541d094c..ab2e5fb08 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -1,34 +1,44 @@ /** Angular Imports */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; /** Custom Components */ -import { FileUploadComponent } from './file-upload/file-upload.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 */ -import { MaterialModule } from './material.module'; import { IconsModule } from './icons.module'; +import { MaterialModule } from './material.module'; /** * Shared Module + * * Modules and components that are shared throughout the application should be here. */ @NgModule({ imports: [ CommonModule, IconsModule, - MaterialModule + MaterialModule, + ReactiveFormsModule ], declarations: [ DeleteDialogComponent, - FileUploadComponent + FileUploadComponent, + FooterComponent, + LanguageSelectorComponent ], exports: [ FileUploadComponent, + FooterComponent, + LanguageSelectorComponent, CommonModule, IconsModule, - MaterialModule + MaterialModule, + ReactiveFormsModule ], entryComponents: [ DeleteDialogComponent