initial codebase
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Cordova
|
||||||
|
/www
|
||||||
|
/plugins
|
||||||
|
/platforms
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/*
|
||||||
|
!.idea/runConfigurations/
|
||||||
|
!.idea/codeStyleSettings.xml
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
xcuserdata/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
/target
|
||||||
|
/log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
/reports
|
||||||
|
/src/translations/template.*
|
||||||
|
/src/environments/.env.*
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"tagname-lowercase": false,
|
||||||
|
"attr-lowercase": false,
|
||||||
|
"attr-value-double-quotes": true,
|
||||||
|
"tag-pair": true,
|
||||||
|
"spec-char-escape": true,
|
||||||
|
"id-unique": true,
|
||||||
|
"src-not-empty": true,
|
||||||
|
"attr-no-duplication": true,
|
||||||
|
"title-require": true,
|
||||||
|
"tag-self-close": true,
|
||||||
|
"head-script-disabled": true,
|
||||||
|
"doctype-html5": true,
|
||||||
|
"id-class-value": "dash",
|
||||||
|
"style-disabled": true,
|
||||||
|
"inline-style-disabled": true,
|
||||||
|
"inline-script-disabled": true,
|
||||||
|
"space-tab-mixed-disabled": "true",
|
||||||
|
"id-class-ad-disabled": true,
|
||||||
|
"attr-unsafe-chars": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"extends": ["stylelint-config-standard", "stylelint-config-recommended-scss"],
|
||||||
|
"rules": {
|
||||||
|
"font-family-name-quotes": "always-where-recommended",
|
||||||
|
"function-url-quotes": [
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"except": ["empty"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selector-attribute-quotes": "always",
|
||||||
|
"string-quotes": "double",
|
||||||
|
"max-nesting-depth": 3,
|
||||||
|
"selector-max-compound-selectors": 3,
|
||||||
|
"selector-max-specificity": "0,3,2",
|
||||||
|
"declaration-no-important": true,
|
||||||
|
"at-rule-no-vendor-prefix": true,
|
||||||
|
"media-feature-name-no-vendor-prefix": true,
|
||||||
|
"property-no-vendor-prefix": true,
|
||||||
|
"selector-no-vendor-prefix": true,
|
||||||
|
"value-no-vendor-prefix": true,
|
||||||
|
"no-empty-source": null,
|
||||||
|
"selector-class-pattern": "[a-z-]+",
|
||||||
|
"selector-id-pattern": "[a-z-]+",
|
||||||
|
"selector-max-id": 0,
|
||||||
|
"selector-no-qualifying-type": true,
|
||||||
|
"selector-max-universal": 0,
|
||||||
|
"selector-pseudo-element-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignorePseudoElements": ["ng-deep"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unit-whitelist": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"],
|
||||||
|
"max-empty-lines": 2,
|
||||||
|
"max-line-length": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# MifosX
|
||||||
|
|
||||||
|
Revamped codebase of community-app
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
1. Go to project folder and install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Launch development server, and open `localhost:4200` in your browser:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
# Main tasks
|
||||||
|
|
||||||
|
Task automation is based on [NPM scripts](https://docs.npmjs.com/misc/scripts).
|
||||||
|
|
||||||
|
Tasks | Description
|
||||||
|
------------------------------|---------------------------------------------------------------------------------------
|
||||||
|
npm start | Run development server on `http://localhost:4200/`
|
||||||
|
npm run build [-- --env=prod] | Lint code and build app for production in `dist/` folder
|
||||||
|
npm test | Run unit tests via [Karma](https://karma-runner.github.io) in watch mode
|
||||||
|
npm run test:ci | Lint code and run unit tests once for continuous integration
|
||||||
|
npm run e2e | Run e2e tests using [Protractor](http://www.protractortest.org)
|
||||||
|
npm run lint | Lint code
|
||||||
|
npm run translations:extract | Extract strings from code and templates to `src/app/translations/template.json`
|
||||||
|
npm run docs | Display project documentation
|
||||||
|
|
||||||
|
When building the application, you can specify the target environment using the additional flag `--env <name>` (do not
|
||||||
|
forget to prepend `--` to pass arguments to npm scripts).
|
||||||
|
|
||||||
|
The default build environment is `prod`.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change
|
||||||
|
any of the source files.
|
||||||
|
You should not use `ng serve` directly, as it does not use the backend proxy configuration by default.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `npm run generate -- component <name>` to generate a new component. You can also use
|
||||||
|
`npm run generate -- directive|pipe|service|class|module`.
|
||||||
|
|
||||||
|
If you have installed [angular-cli](https://github.com/angular/angular-cli) globally with `npm install -g @angular/cli`,
|
||||||
|
you can also use the command `ng generate` directly.
|
||||||
|
|
||||||
|
## Additional tools
|
||||||
|
|
||||||
|
Tasks are mostly based on the `angular-cli` tool. Use `ng help` to get more help or go check out the
|
||||||
|
[Angular-CLI README](https://github.com/angular/angular-cli).
|
||||||
|
|
||||||
|
# What's in the box
|
||||||
|
|
||||||
|
The app template is based on [HTML5](http://whatwg.org/html), [TypeScript](http://www.typescriptlang.org) and
|
||||||
|
[Sass](http://sass-lang.com).
|
||||||
|
|
||||||
|
#### Design guides
|
||||||
|
|
||||||
|
- [Bootstrap Material](https://bootswatch.com/materia/)
|
||||||
|
- [Angular Material](https://material.angular.io/components/categories)
|
||||||
|
|
||||||
|
|
||||||
|
#### Steps to add component
|
||||||
|
|
||||||
|
1. Create a module using
|
||||||
|
`ng generate module [name]`
|
||||||
|
2. Go to *app.module.ts*
|
||||||
|
- import the module
|
||||||
|
|
||||||
|
|
||||||
|
import { module_name } from './module'
|
||||||
|
|
||||||
|
- declare the module in
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
@NgModule({
|
||||||
|
imports: [ module_name ]})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Setup routing in newly created module
|
||||||
|
|
||||||
|
|
||||||
|
#### Coding guides
|
||||||
|
|
||||||
|
- [Angular](docs/coding-guides/angular.md)
|
||||||
|
- [TypeScript](docs/coding-guides/typescript.md)
|
||||||
|
- [Sass](docs/coding-guides/sass.md)
|
||||||
|
- [HTML](docs/coding-guides/html.md)
|
||||||
|
- [Unit tests](docs/coding-guides/unit-tests.md)
|
||||||
|
- [End-to-end tests](docs/coding-guides/e2e-tests.md)
|
||||||
|
|
||||||
|
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"ng-x-rocket": {
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"styleext": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"tsConfig": "src/tsconfig.app.json",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/apple-touch-icon.png",
|
||||||
|
"src/robots.txt",
|
||||||
|
"src/manifest.json",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/main.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"aot": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"serviceWorker": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "ng-x-rocket:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "ng-x-rocket:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "ng-x-rocket:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"karmaConfig": "src/karma.conf.js",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
|
"scripts": [],
|
||||||
|
"styles": [
|
||||||
|
"src/main.scss"
|
||||||
|
],
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/apple-touch-icon.png",
|
||||||
|
"src/robots.txt",
|
||||||
|
"src/manifest.json",
|
||||||
|
"src/assets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"src/tsconfig.app.json",
|
||||||
|
"src/tsconfig.spec.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ng-x-rocket-e2e": {
|
||||||
|
"root": "e2e",
|
||||||
|
"projectType": "application",
|
||||||
|
"architect": {
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "ng-x-rocket:serve"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"e2e/tsconfig.e2e.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "ng-x-rocket"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Analytics
|
||||||
|
|
||||||
|
This project does not come with any analytics library.
|
||||||
|
Should you decide to use one, you may want to consider [Angulartics2](https://github.com/angulartics/angulartics2).
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Backend proxy
|
||||||
|
|
||||||
|
Usually when working on a web application you consume data from custom-made APIs.
|
||||||
|
|
||||||
|
To ease development with our development server integrating live reload while keeping your backend API calls working,
|
||||||
|
we also have setup a backend proxy to redirect API calls to whatever URL and port you want. This allows you:
|
||||||
|
|
||||||
|
- To develop frontend features without the need to run an API backend locally
|
||||||
|
- To use a local development server without [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issues
|
||||||
|
- To debug frontend code with data from a remote testing platform directly
|
||||||
|
|
||||||
|
## How to configure
|
||||||
|
|
||||||
|
In the root folder you will find a `proxy.conf.js`, containing the backend proxy configuration.
|
||||||
|
|
||||||
|
The interesting part is there:
|
||||||
|
```js
|
||||||
|
const proxyConfig = [
|
||||||
|
{
|
||||||
|
context: '/api',
|
||||||
|
pathRewrite: {'^/api': ''},
|
||||||
|
target: 'http://api.icndb.com',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
This is where you can setup one or more proxy rules.
|
||||||
|
|
||||||
|
For the complete set of options, see the `http-proxy-middleware`
|
||||||
|
[documentation](https://github.com/chimurai/http-proxy-middleware#options).
|
||||||
|
|
||||||
|
### Corporate proxy support
|
||||||
|
|
||||||
|
To allow external API calls redirection through a corporate proxy, you will also find a `setupForCorporateProxy()`
|
||||||
|
function in the proxy configuration file. By default, this method configures a corporate proxy agent based on the
|
||||||
|
`HTTP_PROXY` environment variable, see the [corporate proxy documentation](corporate-proxy.md) for more details.
|
||||||
|
|
||||||
|
If you need to, you can further customize this function to fit the network of your working environment.
|
||||||
|
|
||||||
|
If your corporate proxy use a custom SSL certificate, your may need to add the `secure: false` option to your
|
||||||
|
backend proxy configuration.
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# Introduction to Angular and modern design patterns
|
||||||
|
|
||||||
|
[Angular](https://angular.io) (aka Angular 2, 4, 5, 6...) is a new framework completely rewritten from the ground up,
|
||||||
|
replacing the now well-known [AngularJS](https://angularjs.org) framework (aka Angular 1.x).
|
||||||
|
|
||||||
|
More that just a framework, Angular should now be considered as a whole *platform* which comes with a complete set of
|
||||||
|
tools, like its own [CLI](https://github.com/angular/angular-cli), [debug utilities](https://augury.angular.io) or
|
||||||
|
[performance tools](https://github.com/angular/angular/tree/master/packages/benchpress).
|
||||||
|
|
||||||
|
Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved,
|
||||||
|
probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can
|
||||||
|
definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid
|
||||||
|
frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools,
|
||||||
|
patterns and documentation. Yes, the Angular learning curve may seems a little steep, but it’s definitely worth it.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
#### Newcomer
|
||||||
|
|
||||||
|
If you're new to Angular you may feel overwhelmed by the quantity of new concepts to apprehend, so before digging
|
||||||
|
into this project you may want to start with [this progressive tutorial](https://angular.io/tutorial) that will guide
|
||||||
|
you step by step into building a complete Angular application.
|
||||||
|
|
||||||
|
#### AngularJS veteran
|
||||||
|
|
||||||
|
If you come from AngularJS and want to dig straight in the new version, you may want to take a look at the
|
||||||
|
[AngularJS vs 2 quick reference](https://angular.io/guide/ajs-quick-reference).
|
||||||
|
|
||||||
|
#### Cheatsheet
|
||||||
|
|
||||||
|
Until you know the full Angular API by heart, you may want to keep this
|
||||||
|
[cheatsheet](https://angular.io/guide/cheatsheet) that resumes the syntax and features on a single page at hand.
|
||||||
|
|
||||||
|
## Style guide
|
||||||
|
|
||||||
|
This project follows the standard [Angular style guide](https://angular.io/guide/styleguide).
|
||||||
|
|
||||||
|
More that just coding rules, this style guide also gives advices and best practices for a good application architecture
|
||||||
|
and is an **essential reading** for starters. Reading deeper, you can even find many explanations for some design
|
||||||
|
choices of the framework.
|
||||||
|
|
||||||
|
## Going deeper
|
||||||
|
|
||||||
|
Even though they are not mandatory, Angular was designed for the use of design patterns you may not be accustomed to,
|
||||||
|
like [reactive programming](#reactive-programming), [unidirectional data flow](#unidirectional-data-flow) and
|
||||||
|
[centralized state management](#centralized-state-management).
|
||||||
|
|
||||||
|
These concepts are difficult to resume in a few words, and despite being tightly related to each other they concern
|
||||||
|
specific parts of an application flow, each being quite deep to learn on its own.
|
||||||
|
|
||||||
|
You will essentially find here a list of good starting points to learn more on these subjects.
|
||||||
|
|
||||||
|
#### Reactive programming
|
||||||
|
|
||||||
|
You may not be aware of it, but Angular is now a *reactive system* by design.
|
||||||
|
Although you are not forced to use reactive programming patterns, they make the core of the framework and it is
|
||||||
|
definitely recommended to learn them if you want to leverage the best of Angular.
|
||||||
|
|
||||||
|
Angular uses [RxJS](http://reactivex.io/rxjs/) to implement the *Observable* pattern.
|
||||||
|
|
||||||
|
> An *Observable* is a stream of asynchronous events that can be processed with array-like operators.
|
||||||
|
|
||||||
|
##### From promises to observables
|
||||||
|
|
||||||
|
While AngularJS used to rely heavily on [*Promises*](https://docs.angularjs.org/api/ng/service/$q) to handle
|
||||||
|
asynchronous events, *Observables* are now used instead in Angular. Even though in specific cases like for HTTP
|
||||||
|
requests, an *Observable* can be converted into a *Promise*, it is recommended to embrace the new paradigm as it can a
|
||||||
|
lot more than *Promises*, with way less code. This transition is also explained in the
|
||||||
|
[Angular tutorial](https://angular.io/tutorial/toh-pt6#!%23observables).
|
||||||
|
Once you have made the switch, you will never look back again.
|
||||||
|
|
||||||
|
##### Learning references
|
||||||
|
|
||||||
|
- [What is reactive programming?](http://paulstovell.com/blog/reactive-programming), explained nicely through a simple
|
||||||
|
imaged story *(5 min)*
|
||||||
|
|
||||||
|
- [The introduction to reactive programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754),
|
||||||
|
the title says it all *(30 min)*
|
||||||
|
|
||||||
|
- [Functional reactive programming for Angular 2 developers](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/),
|
||||||
|
see the functional reactive programming principles in practice with Angular *(15 min)*
|
||||||
|
|
||||||
|
- [RxMarbles](http://rxmarbles.com), a graphical representation of Rx operators that greatly help to understand their
|
||||||
|
usage
|
||||||
|
|
||||||
|
#### Unidirectional data flow
|
||||||
|
|
||||||
|
In opposition with AngularJS where one of its selling points was two-way data binding which ended up causing a lot of
|
||||||
|
major headaches for complex applications, Angular now enforces unidirectional data flow.
|
||||||
|
|
||||||
|
What does it means? Said with other words, it means that change detection cannot cause cycles, which was one of
|
||||||
|
AngularJS problematic points. It also helps to maintain simpler and more predictable data flows in applications, along
|
||||||
|
with substantial performance improvements.
|
||||||
|
|
||||||
|
**Wait, then why the Angular documentation have mention of a
|
||||||
|
[two-way binding syntax](https://angular.io/guide/template-syntax#binding-syntax-an-overview)?**
|
||||||
|
|
||||||
|
If you look closely, the new two-way binding syntax is just syntactic sugar to combine two *one-way* bindings (a
|
||||||
|
*property* and *event* binding), keeping the data flow unidirectional.
|
||||||
|
|
||||||
|
This change is really important, as it was often the cause of performance issues with AngularJS, and it one of the
|
||||||
|
pillars enabling better performance in new Angular apps.
|
||||||
|
|
||||||
|
While Angular tries to stay *pattern-agnostic* and can be used with conventional MV* patterns, it was designed with
|
||||||
|
reactive programming in mind and really shines when used with reactive data flow patterns like
|
||||||
|
[redux](http://redux.js.org/docs/basics/DataFlow.html),
|
||||||
|
[Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) or
|
||||||
|
[MVI](http://futurice.com/blog/reactive-mvc-and-the-virtual-dom).
|
||||||
|
|
||||||
|
#### Centralized state management
|
||||||
|
|
||||||
|
As applications grow in size, keeping track of the all its individual components state and data flows can become
|
||||||
|
tedious, and tend to be difficult to manage and debug.
|
||||||
|
|
||||||
|
The main goal of using a centralized state management is to make state changes *predictable* by imposing certain
|
||||||
|
restrictions on how and when updates can happen, using *unidirectional data flow*.
|
||||||
|
|
||||||
|
This approach was first made popular with React with introduction of the
|
||||||
|
[Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) architecture. Many libraries emerged then
|
||||||
|
trying to adapt and refine the original concept, and one of these gained massive popularity by providing a simpler,
|
||||||
|
elegant alternative: [Redux](http://redux.js.org/docs/basics/DataFlow.html).
|
||||||
|
|
||||||
|
Redux is at the same time a library (with the big *R*) and a design pattern (with the little *r*), the latter being
|
||||||
|
framework-agnostic and working very well with Angular.
|
||||||
|
|
||||||
|
The *redux* design pattern is based on these [3 principles](http://redux.js.org/docs/introduction/ThreePrinciples.html):
|
||||||
|
|
||||||
|
- The application state is a *single immutable* data structure
|
||||||
|
- A state change is triggered by an *action*, an object describing what happened
|
||||||
|
- Pure functions called *reducers* take the previous state and the next action to compute the new state
|
||||||
|
|
||||||
|
The core concepts behind these principles are nicely explained in
|
||||||
|
[this example](http://redux.js.org/docs/introduction/CoreConcepts.html) *(3 min)*.
|
||||||
|
|
||||||
|
##### Which library to use?
|
||||||
|
|
||||||
|
You can make Angular work with any state management library you like, but your best bet would be to use
|
||||||
|
[@ngrx/store](https://github.com/ngrx/store). It works the same as the popular [Redux](http://redux.js.org) library,
|
||||||
|
but with a tight integration with Angular and [RxJS](http://reactivex.io/rxjs/), with some nice additional developer
|
||||||
|
utilities.
|
||||||
|
|
||||||
|
Here are some resources to get started:
|
||||||
|
|
||||||
|
- [Build a better Angular 2 application with redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/),
|
||||||
|
a nice tutorial for @ngrx/store *(30 min)*
|
||||||
|
|
||||||
|
- [Comprehensive introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510), an in-depth
|
||||||
|
walkthrough to this library usage in Angular *(60 min)*
|
||||||
|
|
||||||
|
##### When to use it?
|
||||||
|
|
||||||
|
You may have noticed that the starter template does not include a centralized state management system out of the box.
|
||||||
|
Why is that? Well, while there is many benefits from using this pattern, the choice is ultimately up to your team and
|
||||||
|
what you want to achieve with your app.
|
||||||
|
|
||||||
|
Keep in mind that using a single centralized state for your app introduces a new layer a complexity
|
||||||
|
[that might not be needed](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367), depending of your
|
||||||
|
goal.
|
||||||
|
|
||||||
|
## Optimizing performance
|
||||||
|
|
||||||
|
While the new Angular version resolves by design most of the performance issues that could be experienced with
|
||||||
|
AngularJS, there is always room for improvements. Just keep in mind that delivering an app with good performance is
|
||||||
|
often a matter of common sense and sane development practices.
|
||||||
|
|
||||||
|
Here is [a list of key points](https://github.com/mgechev/angular-performance-checklist) to check for in your app to
|
||||||
|
make sure you deliver the best experience to your customers.
|
||||||
|
|
||||||
|
After going through the checklist, make sure to also run an audit of your page through
|
||||||
|
[**Lighthouse**](https://developers.google.com/web/tools/lighthouse/), the latest Google tool that gives you meaningful
|
||||||
|
insight about your app performance, accessibility, mobile compatibility and more.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# End-to-end tests coding guide
|
||||||
|
|
||||||
|
End-to-end (E2E for short) tests are meant to test the behavior of your application, from start to finish.
|
||||||
|
|
||||||
|
While unit tests are the first choice for catching bugs and regression on individual components, it is a good idea to
|
||||||
|
complement them with test cases covering the integration between the individual components, hence the need for E2E
|
||||||
|
tests.
|
||||||
|
|
||||||
|
These tests use [Protractor](https://github.com/angular/protractor), which is a framework built for Angular on top of
|
||||||
|
[Selenium](https://github.com/SeleniumHQ/selenium) to control browsers and simulate user inputs.
|
||||||
|
[Jasmine](http://jasmine.github.io) is used as the base test framework.
|
||||||
|
|
||||||
|
## Good practices
|
||||||
|
|
||||||
|
- Avoid whenever possible inter-dependencies between your E2E tests
|
||||||
|
- Run E2E tests on your continuous integration server against different browsers
|
||||||
|
- If you use an Agile methodology, cover each user story acceptance factors with an E2E test
|
||||||
|
|
||||||
|
## Page objects
|
||||||
|
|
||||||
|
E2E tests should follow the *[Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)* pattern.
|
||||||
|
|
||||||
|
#### What is a page object?
|
||||||
|
|
||||||
|
A page object:
|
||||||
|
|
||||||
|
- Models the objects on a page under test:
|
||||||
|
* *Properties* wrap page elements
|
||||||
|
* *Methods* wrap code that interacts with the page elements
|
||||||
|
- Simplifies the test scripts
|
||||||
|
- Reduces the amount of duplicated code
|
||||||
|
|
||||||
|
If the UI changes, the fix only needs to be applied in one place.
|
||||||
|
|
||||||
|
#### How to define a page object
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// login.po.ts
|
||||||
|
import { browser, element, by } from 'protractor';
|
||||||
|
|
||||||
|
export class LoginPage {
|
||||||
|
emailInput = element(by.css('input[name=^"email"]'));
|
||||||
|
passwordInput = element(by.css('input[name=^"password"]'));
|
||||||
|
loginButton = element(by.css('button[(click)^="login"]'));
|
||||||
|
registerButton = element(by.css('button[(click)^="register"]'));
|
||||||
|
|
||||||
|
navigateTo() {
|
||||||
|
return browser.get('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
getGreetingText() {
|
||||||
|
return element(by.css('.greeting')).getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### How to use a page object
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// login.e2e-spec.ts
|
||||||
|
import { LoginPage } from './login.po';
|
||||||
|
|
||||||
|
describe('Login', () => {
|
||||||
|
let page: LoginPage ;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new LoginPage();
|
||||||
|
page.navigateTo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the register page when the register button is clicked', () => {
|
||||||
|
page.registerButton.click();
|
||||||
|
|
||||||
|
expect(browser.getCurrentUrl()).toContain('/register');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow a user to log in', () => {
|
||||||
|
page.emailInput.sendKeys('test@mail.com');
|
||||||
|
page.passwordInput.sendKeys('abc123');
|
||||||
|
page.loginButton.click();
|
||||||
|
|
||||||
|
expect(page.getGreetingText()).toContain('Welcome, Test User');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Parts of this guide were freely inspired by this
|
||||||
|
[presentation](https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# HTML coding guide
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
- Everything should be named in `kebab-case` (lowercase words separated with a `-`): tags, attributes, IDs, etc,
|
||||||
|
**except for everything bound to Angular** such variables, directives or events which should be in `camelCase`
|
||||||
|
- File names should always be in `kebab-case`
|
||||||
|
|
||||||
|
## Coding rules
|
||||||
|
|
||||||
|
- Use HTML5 doctype: `<!doctype html>`
|
||||||
|
- Use HTML [semantic elements](https://developer.mozilla.org/docs/Web/HTML/Sections_and_Outlines_of_an_HTML5_document)
|
||||||
|
- Use double quotes `"` around attribute values in tags
|
||||||
|
- Use a new line for every block, list, or table element, and indent every such child element
|
||||||
|
- Clearly Separate structure (HTML) from presentation (CSS) from behavior (JavaScript):
|
||||||
|
* Never use inline CSS or JavaScript
|
||||||
|
* Keep any logic out of the HTML
|
||||||
|
- `type` attribute for stylesheets and script tags should be omitted
|
||||||
|
|
||||||
|
## Common pitfalls
|
||||||
|
|
||||||
|
- **Block**-type tags cannot be nested inside **inline**-type tags: a `<div>` tag cannot be nested in a `<span>`.
|
||||||
|
This rule also applies regarding the `display` value of an element.
|
||||||
|
- HTML is **not** XML: empty tags cannot be self-closing and will result in improper results
|
||||||
|
* `<div/>` will be interpreted as a simple `<div>` without closing tag!
|
||||||
|
* The only tags that allows self-closing are the one that does not require a closing tag in first place:
|
||||||
|
these are the void elements that do not not accept content `<br>`, `<hr>`, `<img>`, `<input>`, `<meta>`, `<link>`
|
||||||
|
(and others).
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in
|
||||||
|
separate files, when more than 3 lines.
|
||||||
|
|
||||||
|
Only use inline templates sparingly in very simple components with less than 3 lines of HTML.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com).
|
||||||
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Sass coding guide
|
||||||
|
|
||||||
|
[Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large
|
||||||
|
projects and keeping it maintainable.
|
||||||
|
|
||||||
|
The main benefits of using Sass over plain CSS are *variables*, *nesting* and *mixins*, see the
|
||||||
|
[basics guide](http://sass-lang.com/guide) for more details.
|
||||||
|
|
||||||
|
> Note that this project use the newer, CSS-compatible **SCSS** syntax over the old
|
||||||
|
[indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html).
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
- In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`).
|
||||||
|
- File names should always be in `kebab-case`
|
||||||
|
|
||||||
|
## Coding rules
|
||||||
|
|
||||||
|
- Use single quotes `'` for strings
|
||||||
|
- Use this general nesting hierarchy when constructing your styles:
|
||||||
|
```scss
|
||||||
|
// The base component class acts as the namespace, to avoid naming and style collisions
|
||||||
|
.my-component {
|
||||||
|
// Put here all component elements (flat)
|
||||||
|
.my-element {
|
||||||
|
// Use a third-level only for modifiers and state variations
|
||||||
|
&.active { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Note that with
|
||||||
|
[Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation),
|
||||||
|
the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions.
|
||||||
|
|
||||||
|
> As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found
|
||||||
|
it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps
|
||||||
|
keeping everything nicely organized, and more importantly, *scoped*.
|
||||||
|
|
||||||
|
|
||||||
|
Also keep in mind this general rules:
|
||||||
|
- Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible
|
||||||
|
- No more than **3 levels** of nesting
|
||||||
|
- No more than **3 qualifiers**
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
- Use object-oriented CSS (OOCSS):
|
||||||
|
* Factorize common code in base class, and extend it, for example:
|
||||||
|
```scss
|
||||||
|
// Base button class
|
||||||
|
.btn { ... }
|
||||||
|
|
||||||
|
// Color variation
|
||||||
|
.btn-warning { ... }
|
||||||
|
|
||||||
|
// Size variation
|
||||||
|
.btn-small { ... }
|
||||||
|
```
|
||||||
|
* Try to name class by semantic, not style nor function for better reusability:
|
||||||
|
Use `.btn-warning`, not `btn-orange` nor `btn-cancel`
|
||||||
|
* Avoid undoing style, refactor using common base classes and extensions
|
||||||
|
|
||||||
|
- Keep your style scoped
|
||||||
|
* Clearly separate **global** (think *framework*) and **components** style
|
||||||
|
* Global style should only go in `src/theme/`, never in components
|
||||||
|
* Avoid style interactions between components, if some style may need to be shared, refactor it as a framework
|
||||||
|
component in put it in your global theme.
|
||||||
|
* Avoid using wider selectors than needed: always use classes if you can!
|
||||||
|
|
||||||
|
- Avoid rules multiplication
|
||||||
|
* The less CSS the better, factorize rules whenever it's possible
|
||||||
|
* CSS is code, and like any code frequent refactoring is healthy
|
||||||
|
|
||||||
|
- When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in:
|
||||||
|
* These ugly hacks should only be **temporary**
|
||||||
|
* Each hack should be documented with the author name, the problem and hack reason
|
||||||
|
* Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is
|
||||||
|
reached.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Never use the `!important` keyword. Ever.
|
||||||
|
- Never use **inline** style in html, even *just for debugging* (because we **KNOW** it will end up in your commit)
|
||||||
|
|
||||||
|
## Browser compatibility
|
||||||
|
|
||||||
|
You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer)
|
||||||
|
takes care of that part for you during the build process.
|
||||||
|
You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Coding rules are enforced in this project with [stylelint](https://stylelint.io).
|
||||||
|
This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the
|
||||||
|
[`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse).
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# TypeScript coding guide
|
||||||
|
|
||||||
|
[TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web
|
||||||
|
applications.
|
||||||
|
|
||||||
|
Coding conventions and best practices comes from the
|
||||||
|
[TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the
|
||||||
|
[TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html).
|
||||||
|
In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide).
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
- Use `PascalCase` for types, classes, interfaces, constants and enum values.
|
||||||
|
- Use `camelCase` for variables, properties and functions
|
||||||
|
- Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03)
|
||||||
|
- Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this:
|
||||||
|
```typescript
|
||||||
|
private _foo: string;
|
||||||
|
get foo() { return this._foo; } // foo is read-only to consumers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ordering
|
||||||
|
|
||||||
|
- Within a file, type definitions should come first
|
||||||
|
- Within a class, these priorities should be respected:
|
||||||
|
* Properties comes before functions
|
||||||
|
* Static symbols comes before instance symbols
|
||||||
|
* Public symbols comes before private symbols
|
||||||
|
|
||||||
|
## Coding rules
|
||||||
|
|
||||||
|
- Use single quotes `'` for strings
|
||||||
|
- Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see
|
||||||
|
[JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)).
|
||||||
|
The only accepted usage for `==` is when you want to check a value against `null` or `undefined`.
|
||||||
|
- Use `[]` instead of `Array` constructor
|
||||||
|
- Use `{}` instead of `Object` constructor
|
||||||
|
- Always specify types for function parameters and returns (if applicable)
|
||||||
|
- Do not export types/functions unless you need to share it across multiple components
|
||||||
|
- Do not introduce new types/values to the global namespace
|
||||||
|
- Use arrow functions over anonymous function expressions
|
||||||
|
- Only surround arrow function parameters when necessary.
|
||||||
|
For example, `(x) => x + x` is wrong but the following are correct:
|
||||||
|
* `x => x + x`
|
||||||
|
* `(x, y) => x + y`
|
||||||
|
* `<T>(x: T, y: T) => x === y`
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are
|
||||||
|
located in the `node_modules/@types` folder.
|
||||||
|
|
||||||
|
To manage type definitions, use standard `npm install|update|remove` commands.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint).
|
||||||
|
Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions.
|
||||||
|
|
||||||
|
## Learn more
|
||||||
|
|
||||||
|
The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good
|
||||||
|
reference book for TypeScript (and also open-source).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Unit tests coding guide
|
||||||
|
|
||||||
|
The main objective of unit tests is to detect regressions and to help you design software components. A suite of
|
||||||
|
*good* unit tests can be *immensely* valuable for your project and makes it easier to refactor and expand your code.
|
||||||
|
But keep in mind that a suite of *bad* unit tests can also be *immensely* painful, and hurt your development by
|
||||||
|
inhibiting your ability to refactor or alter your code in any way.
|
||||||
|
|
||||||
|
## What to test?
|
||||||
|
|
||||||
|
Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models...
|
||||||
|
Shared directives/components should also be covered by unit tests, if you do not have the time to test every single
|
||||||
|
component.
|
||||||
|
|
||||||
|
Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests
|
||||||
|
cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the
|
||||||
|
integration and interactions with other app components based on real use cases scenarios.
|
||||||
|
|
||||||
|
## Good practices
|
||||||
|
|
||||||
|
- Name your tests cleanly and consistently
|
||||||
|
- Do not only test nominal cases, the most important tests are the one covering the edge cases
|
||||||
|
- Each test should be independent to all the others
|
||||||
|
- Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase
|
||||||
|
pointless failures and maintenance workload
|
||||||
|
- Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app
|
||||||
|
- Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you
|
||||||
|
should split your tested module into several more independent modules
|
||||||
|
- Clearly separate or identify these 3 stages of each unit test (the *3A*): *arrange*, *act* and *assert*
|
||||||
|
- When you fix a bug, add a test case for it to prevent regression
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can,
|
||||||
|
but if you can't, at least make sure each test resets the relevant statics before and after your tests.
|
||||||
|
- Don’t unit-test configuration settings
|
||||||
|
- Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after
|
||||||
|
essential features of your code unit are tested**, your can think of improving the coverage.
|
||||||
|
|
||||||
|
## Unit testing with Angular
|
||||||
|
|
||||||
|
A good starting point for learning is the official
|
||||||
|
[testing guide](https://angular.io/docs/ts/latest/guide/testing.html).
|
||||||
|
|
||||||
|
But as you will most likely want to go bit further in real world apps, these
|
||||||
|
[example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to
|
||||||
|
learn how to cover most common testing use cases.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Working behind a corporate proxy
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Most tools (including npm and git) use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to work with a
|
||||||
|
corporate proxy.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
In Windows environments, add the `HTTP_PROXY` and `HTTPS_PROXY` system environment variable, with these values:
|
||||||
|
|
||||||
|
- HTTP_PROXY: `http://<username>:<password>@<proxy_server>:<proxy_port>`
|
||||||
|
- HTTPS_PROXY: `%HTTP_PROXY%`
|
||||||
|
|
||||||
|
### Unix
|
||||||
|
|
||||||
|
Add these lines to your `~/.bash_profile` or `~/.profile`:
|
||||||
|
```sh
|
||||||
|
export HTTP_PROXY="http://<username>:<password>@<proxy_server>:<proxy_port>"
|
||||||
|
export HTTPS_PROXY="$HTTP_PROXY"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proxy with SSL custom certificate
|
||||||
|
|
||||||
|
Some proxy like **zscaler** use a custom SSL certificate to inspect request, which may cause npm commands to fail.
|
||||||
|
|
||||||
|
To solve this problem, you can disable the `strict-ssl` option in npm.
|
||||||
|
|
||||||
|
## Proxy exceptions
|
||||||
|
|
||||||
|
If you need to access repositories on your local network that should bypass proxy, set the `NO_PROXY` environment
|
||||||
|
variable, in the same way as `HTTP_PROXY`:
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- NO_PROXY: `127.0.0.1, localhost, <your_local_server_ip_or_hostname>`
|
||||||
|
|
||||||
|
### Unix
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export NO_PROXY="127.0.0.1, localhost, <your_local_server_ip_or_hostname>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Npm
|
||||||
|
|
||||||
|
Run this command in your project directory:
|
||||||
|
```sh
|
||||||
|
npm set strict-ssl false
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# I18n
|
||||||
|
|
||||||
|
The internationalization of the application is managed by [ngx-translate](https://github.com/ngx-translate/core).
|
||||||
|
|
||||||
|
## Adding translatable strings
|
||||||
|
|
||||||
|
### In HTML templates
|
||||||
|
|
||||||
|
Use the `translate` directive on an HTML element to automatically translate its content:
|
||||||
|
```html
|
||||||
|
<span translate>This text will be translated.</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the `translate` pipe if needed:
|
||||||
|
```html
|
||||||
|
<button title="{{ 'Add item' | translate }}">+</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### In TypeScript code
|
||||||
|
|
||||||
|
If you need to translate strings in JavaScript code, import the `TranslateService` dependency and use the asynchronous
|
||||||
|
`get()` method:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let title;
|
||||||
|
translateService.get('My page title').subscribe((res: string) => { title = res; });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extracting strings to translate
|
||||||
|
|
||||||
|
Once you are ready to translate your app, just run `npm translations:extract`.
|
||||||
|
It will create a `template.json` file in the `src/translations` folder.
|
||||||
|
|
||||||
|
You can then use any text or code editor to generate the `.json` files for each of your supported languages, and put
|
||||||
|
them in the `src/translations` folder.
|
||||||
|
|
||||||
|
Do no forget to edit the files in `src/environment` to add the supported languages of your application.
|
||||||
|
|
||||||
|
### Marking strings for extraction
|
||||||
|
|
||||||
|
If strings are not directly passed to `translateService` or put in HTML templates, they may be missing from the
|
||||||
|
extraction process.
|
||||||
|
|
||||||
|
For these cases, you have to use the dummy `extract()` function:
|
||||||
|
```typescript
|
||||||
|
import { extract } from './core/i18n.service';
|
||||||
|
|
||||||
|
function toBeTranslatedLater() {
|
||||||
|
return extract('A string to be translated');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Strings marked like this will then be properly extracted.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ngX-Rocket
|
||||||
|
|
||||||
|
Welcome to the project documentation!
|
||||||
|
|
||||||
|
Use `npm run docs` for easier navigation.
|
||||||
|
|
||||||
|
## Available documentation
|
||||||
|
|
||||||
|
[[index]]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Browser routing
|
||||||
|
|
||||||
|
To allow navigation without triggering a server request, Angular now use by default the
|
||||||
|
[HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries)
|
||||||
|
API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the *hashbang* hack
|
||||||
|
routing style (like `localhost:4200/#/home/`).
|
||||||
|
|
||||||
|
This change has several consequences you should know of, be sure to read the
|
||||||
|
[browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully
|
||||||
|
understand the differences between the two approaches.
|
||||||
|
|
||||||
|
In short:
|
||||||
|
|
||||||
|
- It is only supported on modern browsers (IE10+), a [polyfill](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#html5-history-api-pushstate-replacestate-popstate)
|
||||||
|
is required for older browsers.
|
||||||
|
|
||||||
|
- You have the option to perform *server-side rendering* later if you need to increase your app perceived performance.
|
||||||
|
|
||||||
|
- You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file.
|
||||||
|
|
||||||
|
It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the
|
||||||
|
default HTML5 routing mode.
|
||||||
|
|
||||||
|
## Server configuration
|
||||||
|
|
||||||
|
To allow your angular application working properly as a *Single Page Application* (SPA) and allow bookmarking or
|
||||||
|
refreshing any page, you need some configuration on your server, otherwise you will be running into troubles.
|
||||||
|
|
||||||
|
> Note that during development, the live reload server already supports SPA mode.
|
||||||
|
|
||||||
|
The basic idea is simply to serve the `index.html` file for every request aimed at your application.
|
||||||
|
|
||||||
|
Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Put this in your `server.js` file, after your other rules (APIs, static files...)
|
||||||
|
app.get('/*', function(req, res) {
|
||||||
|
res.sendFile(__dirname + '/index.html')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or
|
||||||
|
[Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform *URL rewriting*.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Updating npm dependencies
|
||||||
|
|
||||||
|
- Check outdated packages
|
||||||
|
```sh
|
||||||
|
npm outdated
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update local packages according to `package.json`
|
||||||
|
```sh
|
||||||
|
npm update
|
||||||
|
```
|
||||||
|
|
||||||
|
- Upgrade packages manually
|
||||||
|
```sh
|
||||||
|
npm install --save[-dev] <package_name>@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use [npm-check](https://github.com/dylang/npm-check) to perform an interactive upgrade:
|
||||||
|
```sh
|
||||||
|
npm-check -u --skip-unused
|
||||||
|
```
|
||||||
|
|
||||||
|
## Locking package versions
|
||||||
|
|
||||||
|
Starting from `npm@5` a new `package-lock.json` file is
|
||||||
|
[automatically generated](https://docs.npmjs.com/files/package-locks) when using `npm install` commands, to ensure a
|
||||||
|
reproducible dependency tree and avoid unwanted package updates.
|
||||||
|
|
||||||
|
If you use a previous npm version, it is recommended to use [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) to
|
||||||
|
lock down all your dependencies version:
|
||||||
|
```sh
|
||||||
|
npm shrinkwrap --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a file `npm-shrinkwrap.json` alongside your `package.json` files.
|
||||||
|
|
||||||
|
> Do not forget to run shrinkwrap each time you manually update your dependencies!
|
||||||
|
|
||||||
|
# Updating angular-cli
|
||||||
|
|
||||||
|
The `angular-cli` package needs extra care with updating, see
|
||||||
|
[this doc](https://github.com/angular/angular-cli#updating-angular-cli).
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
'browserName': process.env.PROTRACTOR_BROWSER || 'chrome'
|
||||||
|
},
|
||||||
|
// Only works with Chrome and Firefox
|
||||||
|
directConnect: true,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine2',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 30000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||||
|
});
|
||||||
|
// Better console spec reporter
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { browser } from 'protractor';
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('app', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display login page and login into app', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(browser.getCurrentUrl()).toContain('/login');
|
||||||
|
page.login();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display hello message', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getParagraphText()).toEqual('Hello world !');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Use the Page Object pattern to define the page under test.
|
||||||
|
* See docs/coding-guide/e2e-tests.md for more info.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browser, element, by } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
usernameField = element(by.css('input[formControlName="username"]'));
|
||||||
|
passwordField = element(by.css('input[formControlName="password"]'));
|
||||||
|
loginButton = element(by.css('button[type="submit"]'));
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Forces default language
|
||||||
|
this.navigateTo();
|
||||||
|
browser.executeScript(() => localStorage.setItem('language', 'en-US'));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo() {
|
||||||
|
return browser.get('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.usernameField.sendKeys('test');
|
||||||
|
this.passwordField.sendKeys('123');
|
||||||
|
this.loginButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
getParagraphText() {
|
||||||
|
return element(by.css('app-root h1')).getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"index": "/index.html",
|
||||||
|
"assetGroups": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"installMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/index.html",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"installMode": "lazy",
|
||||||
|
"updateMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/assets/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+14082
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "ng-x-rocket",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"build": "npm run env -s && ng build --prod",
|
||||||
|
"start": "npm run env -s && ng serve --aot --proxy-config proxy.conf.js",
|
||||||
|
"serve:sw": "npm run build -s && npx http-server ./dist -p 4200",
|
||||||
|
"lint": "ng lint && stylelint \"src/**/*.scss\" --syntax scss && htmlhint \"src\" --config .htmlhintrc",
|
||||||
|
"test": "npm run env -s && ng test",
|
||||||
|
"test:ci": "npm run env -s && npm run lint -s && ng test --code-coverage --watch=false",
|
||||||
|
"e2e": "npm run env -s && ng e2e",
|
||||||
|
"translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean -sort --marker extract",
|
||||||
|
"docs": "hads ./docs -o",
|
||||||
|
"env": "ngx-scripts env npm_package_version",
|
||||||
|
"generate": "ng generate"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^6.0.3",
|
||||||
|
"@angular/cdk": "^6.1.0",
|
||||||
|
"@angular/common": "^6.0.1",
|
||||||
|
"@angular/compiler": "^6.0.1",
|
||||||
|
"@angular/core": "^6.0.1",
|
||||||
|
"@angular/forms": "^6.0.1",
|
||||||
|
"@angular/http": "^6.0.1",
|
||||||
|
"@angular/material": "^6.1.0",
|
||||||
|
"@angular/platform-browser": "^6.0.1",
|
||||||
|
"@angular/platform-browser-dynamic": "^6.0.1",
|
||||||
|
"@angular/router": "^6.0.1",
|
||||||
|
"@angular/service-worker": "^6.0.1",
|
||||||
|
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
|
||||||
|
"@ngx-translate/core": "^10.0.1",
|
||||||
|
"bootstrap": "^4.1.1",
|
||||||
|
"core-js": "^2.5.6",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"rxjs": "^6.1.0",
|
||||||
|
"zone.js": "^0.8.26"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/cli": "~6.0.1",
|
||||||
|
"@angular/compiler-cli": "^6.0.1",
|
||||||
|
"@angular/language-service": "^6.0.1",
|
||||||
|
"@angular-devkit/build-angular": "~0.6.0",
|
||||||
|
"@biesbjerg/ngx-translate-extract": "^2.3.4",
|
||||||
|
"@ngx-rocket/scripts": "^2.1.0",
|
||||||
|
"@types/jasmine": "^2.8.7",
|
||||||
|
"@types/jasminewd2": "^2.0.2",
|
||||||
|
"@types/lodash": "^4.14.108",
|
||||||
|
"@types/node": "^8.10.13",
|
||||||
|
"codelyzer": "^4.3.0",
|
||||||
|
"hads": "^1.3.4",
|
||||||
|
"htmlhint": "^0.9.13",
|
||||||
|
"https-proxy-agent": "^2.0.0",
|
||||||
|
"jasmine-core": "~2.99.1",
|
||||||
|
"jasmine-spec-reporter": "^4.1.0",
|
||||||
|
"karma": "~2.0.0",
|
||||||
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-cli": "~1.0.1",
|
||||||
|
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||||
|
"karma-jasmine": "^1.1.2",
|
||||||
|
"karma-junit-reporter": "^1.2.0",
|
||||||
|
"protractor": "~5.3.0",
|
||||||
|
"puppeteer": "^1.4.0",
|
||||||
|
"stylelint": "~9.2.0",
|
||||||
|
"stylelint-config-recommended-scss": "~3.2.0",
|
||||||
|
"stylelint-config-standard": "~18.2.0",
|
||||||
|
"stylelint-scss": "~3.1.0",
|
||||||
|
"ts-node": "~6.0.3",
|
||||||
|
"tslint": "~5.10.0",
|
||||||
|
"typescript": "~2.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const HttpsProxyAgent = require('https-proxy-agent');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* API proxy configuration.
|
||||||
|
* This allows you to proxy HTTP request like `http.get('/api/stuff')` to another server/port.
|
||||||
|
* This is especially useful during app development to avoid CORS issues while running a local server.
|
||||||
|
* For more details and options, see https://github.com/angular/angular-cli#proxy-to-backend
|
||||||
|
*/
|
||||||
|
const proxyConfig = [
|
||||||
|
{
|
||||||
|
context: '/api',
|
||||||
|
pathRewrite: { '^/api': '' },
|
||||||
|
target: 'https://api.chucknorris.io',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configures a corporate proxy agent for the API proxy if needed.
|
||||||
|
*/
|
||||||
|
function setupForCorporateProxy(proxyConfig) {
|
||||||
|
if (!Array.isArray(proxyConfig)) {
|
||||||
|
proxyConfig = [proxyConfig];
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyServer = process.env.http_proxy || process.env.HTTP_PROXY;
|
||||||
|
let agent = null;
|
||||||
|
|
||||||
|
if (proxyServer) {
|
||||||
|
console.log(`Using corporate proxy server: ${proxyServer}`);
|
||||||
|
agent = new HttpsProxyAgent(proxyServer);
|
||||||
|
proxyConfig.forEach(entry => { entry.agent = agent; });
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = setupForCorporateProxy(proxyConfig);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { Route, extract } from '@app/core';
|
||||||
|
import { AboutComponent } from './about.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
Route.withShell([
|
||||||
|
{ path: 'about', component: AboutComponent, data: { title: extract('About') } }
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class AboutRoutingModule { }
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="jumbotron text-center">
|
||||||
|
<h1>
|
||||||
|
<span translate>APP_NAME</span>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<i class="far fa-bookmark"></i> <span translate>Version</span> {{version}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AboutComponent } from './about.component';
|
||||||
|
|
||||||
|
describe('AboutComponent', () => {
|
||||||
|
let component: AboutComponent;
|
||||||
|
let fixture: ComponentFixture<AboutComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AboutComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AboutComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-about',
|
||||||
|
templateUrl: './about.component.html',
|
||||||
|
styleUrls: ['./about.component.scss']
|
||||||
|
})
|
||||||
|
export class AboutComponent implements OnInit {
|
||||||
|
|
||||||
|
version: string = environment.version;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { AboutRoutingModule } from './about-routing.module';
|
||||||
|
import { AboutComponent } from './about.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule,
|
||||||
|
AboutRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AboutComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AboutModule { }
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
// Fallback when no prior route is matched
|
||||||
|
{ path: '**', redirectTo: '', pathMatch: 'full' },
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CoreModule } from '@app/core';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
CoreModule
|
||||||
|
],
|
||||||
|
declarations: [AppComponent],
|
||||||
|
providers: []
|
||||||
|
});
|
||||||
|
TestBed.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create the app', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { merge } from 'rxjs';
|
||||||
|
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { Logger, I18nService } from '@app/core';
|
||||||
|
|
||||||
|
const log = new Logger('App');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss']
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private router: Router,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private titleService: Title,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private i18nService: I18nService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Setup logger
|
||||||
|
if (environment.production) {
|
||||||
|
Logger.enableProductionMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('init');
|
||||||
|
|
||||||
|
|
||||||
|
// Setup translations
|
||||||
|
this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages);
|
||||||
|
|
||||||
|
const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd));
|
||||||
|
|
||||||
|
// Change page title on navigation or language change, based on route data
|
||||||
|
merge(this.translateService.onLangChange, onNavigationEnd)
|
||||||
|
.pipe(
|
||||||
|
map(() => {
|
||||||
|
let route = this.activatedRoute;
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}),
|
||||||
|
filter(route => route.outlet === 'primary'),
|
||||||
|
mergeMap(route => route.data)
|
||||||
|
)
|
||||||
|
.subscribe(event => {
|
||||||
|
const title = event['title'];
|
||||||
|
if (title) {
|
||||||
|
this.titleService.setTitle(this.translateService.instant(title));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { CoreModule } from '@app/core';
|
||||||
|
import { SharedModule } from '@app/shared';
|
||||||
|
import { HomeModule } from './home/home.module';
|
||||||
|
import { AboutModule } from './about/about.module';
|
||||||
|
import { ClientsModule} from './clients/clients.module'
|
||||||
|
import { LoginModule } from './login/login.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }),
|
||||||
|
FormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
CoreModule,
|
||||||
|
SharedModule,
|
||||||
|
HomeModule,
|
||||||
|
AboutModule,
|
||||||
|
ClientsModule,
|
||||||
|
LoginModule,
|
||||||
|
AppRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [AppComponent,],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { extract } from '@app/core';
|
||||||
|
import { ClientsComponent } from './clients.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'clients', component: ClientsComponent }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class ClientsRoutingModule { }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<app-header> </app-header>
|
||||||
|
<p>
|
||||||
|
clients works!
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
|
import { HeaderComponent } from '../core/shell/header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-clients',
|
||||||
|
templateUrl: './clients.component.html',
|
||||||
|
styleUrls: ['./clients.component.scss']
|
||||||
|
})
|
||||||
|
export class ClientsComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule,CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { ClientsComponent } from './clients.component';
|
||||||
|
import { ClientsRoutingModule } from './clients-routing.module';
|
||||||
|
import { CoreModule } from '../core/core.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreModule,
|
||||||
|
CommonModule,
|
||||||
|
ClientsRoutingModule,
|
||||||
|
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ClientsComponent
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
],
|
||||||
|
|
||||||
|
})
|
||||||
|
export class ClientsModule { }
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
import { MockAuthenticationService } from './authentication.service.mock';
|
||||||
|
import { AuthenticationGuard } from './authentication.guard';
|
||||||
|
|
||||||
|
describe('AuthenticationGuard', () => {
|
||||||
|
let authenticationGuard: AuthenticationGuard;
|
||||||
|
let authenticationService: MockAuthenticationService;
|
||||||
|
let mockRouter: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRouter = {
|
||||||
|
navigate: jasmine.createSpy('navigate')
|
||||||
|
};
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthenticationGuard,
|
||||||
|
{ provide: AuthenticationService, useClass: MockAuthenticationService },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
AuthenticationGuard,
|
||||||
|
AuthenticationService
|
||||||
|
], (_authenticationGuard: AuthenticationGuard,
|
||||||
|
_authenticationService: MockAuthenticationService) => {
|
||||||
|
|
||||||
|
authenticationGuard = _authenticationGuard;
|
||||||
|
authenticationService = _authenticationService;
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should have a canActivate method', () => {
|
||||||
|
expect(typeof authenticationGuard.canActivate).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if user is authenticated', () => {
|
||||||
|
expect(authenticationGuard.canActivate()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false and redirect to login if user is not authenticated', () => {
|
||||||
|
// Arrange
|
||||||
|
authenticationService.credentials = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = authenticationGuard.canActivate();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], {replaceUrl: true});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router, CanActivate } from '@angular/router';
|
||||||
|
|
||||||
|
import { Logger } from '../logger.service';
|
||||||
|
import { AuthenticationService } from './authentication.service';
|
||||||
|
|
||||||
|
const log = new Logger('AuthenticationGuard');
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(private router: Router,
|
||||||
|
private authenticationService: AuthenticationService) { }
|
||||||
|
|
||||||
|
canActivate(): boolean {
|
||||||
|
if (this.authenticationService.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Not authenticated, redirecting...');
|
||||||
|
this.router.navigate(['/login'], { replaceUrl: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { Credentials, LoginContext } from './authentication.service';
|
||||||
|
|
||||||
|
export class MockAuthenticationService {
|
||||||
|
|
||||||
|
credentials: Credentials | null = {
|
||||||
|
username: 'test',
|
||||||
|
token: '123'
|
||||||
|
};
|
||||||
|
|
||||||
|
login(context: LoginContext): Observable<Credentials> {
|
||||||
|
return of({
|
||||||
|
username: context.username,
|
||||||
|
token: '123456'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): Observable<boolean> {
|
||||||
|
this.credentials = null;
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
return !!this.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthenticationService, Credentials} from './authentication.service';
|
||||||
|
|
||||||
|
const credentialsKey = 'credentials';
|
||||||
|
|
||||||
|
describe('AuthenticationService', () => {
|
||||||
|
let authenticationService: AuthenticationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [AuthenticationService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
AuthenticationService
|
||||||
|
], (_authenticationService: AuthenticationService) => {
|
||||||
|
authenticationService = _authenticationService;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup
|
||||||
|
localStorage.removeItem(credentialsKey);
|
||||||
|
sessionStorage.removeItem(credentialsKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should return credentials', fakeAsync(() => {
|
||||||
|
// Act
|
||||||
|
const request = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123'
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(credentials => {
|
||||||
|
expect(credentials).toBeDefined();
|
||||||
|
expect(credentials.token).toBeDefined();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should authenticate user', fakeAsync(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const request = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123'
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(true);
|
||||||
|
expect(authenticationService.credentials).toBeDefined();
|
||||||
|
expect(authenticationService.credentials).not.toBeNull();
|
||||||
|
expect((<Credentials>authenticationService.credentials).token).toBeDefined();
|
||||||
|
expect((<Credentials>authenticationService.credentials).token).not.toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should persist credentials for the session', fakeAsync(() => {
|
||||||
|
// Act
|
||||||
|
const request = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123'
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(sessionStorage.getItem(credentialsKey)).not.toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should persist credentials across sessions', fakeAsync(() => {
|
||||||
|
// Act
|
||||||
|
const request = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123',
|
||||||
|
remember: true
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(localStorage.getItem(credentialsKey)).not.toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logout', () => {
|
||||||
|
it('should clear user authentication', fakeAsync(() => {
|
||||||
|
// Arrange
|
||||||
|
const loginRequest = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123'
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
loginRequest.subscribe(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(true);
|
||||||
|
|
||||||
|
const request = authenticationService.logout();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(false);
|
||||||
|
expect(authenticationService.credentials).toBeNull();
|
||||||
|
expect(sessionStorage.getItem(credentialsKey)).toBeNull();
|
||||||
|
expect(localStorage.getItem(credentialsKey)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should clear persisted user authentication', fakeAsync(() => {
|
||||||
|
// Arrange
|
||||||
|
const loginRequest = authenticationService.login({
|
||||||
|
username: 'toto',
|
||||||
|
password: '123',
|
||||||
|
remember: true
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
loginRequest.subscribe(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(true);
|
||||||
|
|
||||||
|
const request = authenticationService.logout();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(authenticationService.isAuthenticated()).toBe(false);
|
||||||
|
expect(authenticationService.credentials).toBeNull();
|
||||||
|
expect(sessionStorage.getItem(credentialsKey)).toBeNull();
|
||||||
|
expect(localStorage.getItem(credentialsKey)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
export interface Credentials {
|
||||||
|
// Customize received credentials here
|
||||||
|
username: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginContext {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
remember?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialsKey = 'credentials';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a base for authentication workflow.
|
||||||
|
* The Credentials interface as well as login/logout methods should be replaced with proper implementation.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AuthenticationService {
|
||||||
|
|
||||||
|
private _credentials: Credentials | null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const savedCredentials = sessionStorage.getItem(credentialsKey) || localStorage.getItem(credentialsKey);
|
||||||
|
if (savedCredentials) {
|
||||||
|
this._credentials = JSON.parse(savedCredentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates the user.
|
||||||
|
* @param {LoginContext} context The login parameters.
|
||||||
|
* @return {Observable<Credentials>} The user credentials.
|
||||||
|
*/
|
||||||
|
login(context: LoginContext): Observable<Credentials> {
|
||||||
|
// Replace by proper authentication call
|
||||||
|
const data = {
|
||||||
|
username: context.username,
|
||||||
|
token: '123456'
|
||||||
|
};
|
||||||
|
this.setCredentials(data, context.remember);
|
||||||
|
return of(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs out the user and clear credentials.
|
||||||
|
* @return {Observable<boolean>} True if the user was logged out successfully.
|
||||||
|
*/
|
||||||
|
logout(): Observable<boolean> {
|
||||||
|
// Customize credentials invalidation here
|
||||||
|
this.setCredentials();
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks is the user is authenticated.
|
||||||
|
* @return {boolean} True if the user is authenticated.
|
||||||
|
*/
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
return !!this.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user credentials.
|
||||||
|
* @return {Credentials} The user credentials or null if the user is not authenticated.
|
||||||
|
*/
|
||||||
|
get credentials(): Credentials | null {
|
||||||
|
return this._credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user credentials.
|
||||||
|
* The credentials may be persisted across sessions by setting the `remember` parameter to true.
|
||||||
|
* Otherwise, the credentials are only persisted for the current session.
|
||||||
|
* @param {Credentials=} credentials The user credentials.
|
||||||
|
* @param {boolean=} remember True to remember credentials across sessions.
|
||||||
|
*/
|
||||||
|
private setCredentials(credentials?: Credentials, remember?: boolean) {
|
||||||
|
this._credentials = credentials || null;
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
const storage = remember ? localStorage : sessionStorage;
|
||||||
|
storage.setItem(credentialsKey, JSON.stringify(credentials));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem(credentialsKey);
|
||||||
|
localStorage.removeItem(credentialsKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
|
||||||
|
import { RouteReuseStrategy, RouterModule } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { ShellComponent } from './shell/shell.component';
|
||||||
|
import { HeaderComponent } from './shell/header/header.component';
|
||||||
|
import { RouteReusableStrategy } from './route-reusable-strategy';
|
||||||
|
import { AuthenticationService } from './authentication/authentication.service';
|
||||||
|
import { AuthenticationGuard } from './authentication/authentication.guard';
|
||||||
|
import { I18nService } from './i18n.service';
|
||||||
|
import { HttpService } from './http/http.service';
|
||||||
|
import { HttpCacheService } from './http/http-cache.service';
|
||||||
|
import { ApiPrefixInterceptor } from './http/api-prefix.interceptor';
|
||||||
|
import { ErrorHandlerInterceptor } from './http/error-handler.interceptor';
|
||||||
|
import { CacheInterceptor } from './http/cache.interceptor';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
HttpClientModule,
|
||||||
|
TranslateModule,
|
||||||
|
NgbModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
|
||||||
|
declarations: [
|
||||||
|
HeaderComponent,
|
||||||
|
ShellComponent
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
HeaderComponent],
|
||||||
|
|
||||||
|
providers: [
|
||||||
|
AuthenticationService,
|
||||||
|
AuthenticationGuard,
|
||||||
|
I18nService,
|
||||||
|
HttpCacheService,
|
||||||
|
ApiPrefixInterceptor,
|
||||||
|
ErrorHandlerInterceptor,
|
||||||
|
CacheInterceptor,
|
||||||
|
{
|
||||||
|
provide: HttpClient,
|
||||||
|
useClass: HttpService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: RouteReuseStrategy,
|
||||||
|
useClass: RouteReusableStrategy
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CoreModule {
|
||||||
|
|
||||||
|
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||||
|
// Import guard
|
||||||
|
if (parentModule) {
|
||||||
|
throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||||
|
|
||||||
|
describe('ApiPrefixInterceptor', () => {
|
||||||
|
let http: HttpClient;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: ApiPrefixInterceptor,
|
||||||
|
multi: true
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpClient,
|
||||||
|
HttpTestingController
|
||||||
|
], (_http: HttpClient,
|
||||||
|
_httpMock: HttpTestingController) => {
|
||||||
|
|
||||||
|
http = _http;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prepend environment.serverUrl to the request url', () => {
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
httpMock.expectOne({ url: environment.serverUrl + '/toto' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes all requests with `environment.serverUrl`.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ApiPrefixInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
request = request.clone({ url: environment.serverUrl + request.url });
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { CacheInterceptor } from './cache.interceptor';
|
||||||
|
import { HttpCacheService } from './http-cache.service';
|
||||||
|
|
||||||
|
describe('CacheInterceptor', () => {
|
||||||
|
let interceptorOptions: Object | null = {};
|
||||||
|
let httpCacheService: HttpCacheService;
|
||||||
|
let cacheInterceptor: CacheInterceptor;
|
||||||
|
let http: HttpClient;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
function createInterceptor(_httpCacheService: HttpCacheService) {
|
||||||
|
cacheInterceptor = new CacheInterceptor(_httpCacheService).configure(interceptorOptions);
|
||||||
|
return cacheInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
HttpCacheService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useFactory: createInterceptor,
|
||||||
|
deps: [HttpCacheService],
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpCacheService.cleanCache();
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with default configuration', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptorOptions = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpClient,
|
||||||
|
HttpTestingController,
|
||||||
|
HttpCacheService
|
||||||
|
], (_http: HttpClient,
|
||||||
|
_httpMock: HttpTestingController,
|
||||||
|
_httpCacheService: HttpCacheService) => {
|
||||||
|
|
||||||
|
http = _http;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
httpCacheService = _httpCacheService;
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should cache the request', () => {
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe(() => {
|
||||||
|
// Assert
|
||||||
|
const cachedData = httpCacheService.getCacheData('/toto');
|
||||||
|
expect(cachedData).toBeDefined();
|
||||||
|
expect(cachedData ? cachedData.body : null).toEqual('someData');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock.expectOne({ url: '/toto' }).flush('someData');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond from the cache', () => {
|
||||||
|
// Arrange
|
||||||
|
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' }));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe(response => {
|
||||||
|
// Assert
|
||||||
|
expect(response).toEqual('cachedData');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock.expectNone({ url: '/toto' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache the request in case of error', () => {
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe(() => {}, () => {
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/toto')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock.expectOne({}).flush(null, {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with update forced configuration', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptorOptions = { update: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpClient,
|
||||||
|
HttpTestingController,
|
||||||
|
HttpCacheService
|
||||||
|
], (_http: HttpClient,
|
||||||
|
_httpMock: HttpTestingController,
|
||||||
|
_httpCacheService: HttpCacheService) => {
|
||||||
|
|
||||||
|
http = _http;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
httpCacheService = _httpCacheService;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpCacheService.cleanCache();
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should force cache update', () => {
|
||||||
|
// Arrange
|
||||||
|
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' }));
|
||||||
|
cacheInterceptor.configure({ update: true });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe(response => {
|
||||||
|
// Assert
|
||||||
|
expect(response).toEqual('newData');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock.expectOne({ url: '/toto' }).flush('newData');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
|
||||||
|
import { Observable, Subscriber } from 'rxjs';
|
||||||
|
|
||||||
|
import { HttpCacheService } from './http-cache.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches HTTP requests.
|
||||||
|
* Use ExtendedHttpClient fluent API to configure caching for each request.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CacheInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
private forceUpdate = false;
|
||||||
|
|
||||||
|
constructor(private httpCacheService: HttpCacheService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures interceptor options
|
||||||
|
* @param {{update: boolean}} options If update option is enabled, forces request to be made and updates cache entry.
|
||||||
|
* @return {CacheInterceptor} The configured instance.
|
||||||
|
*/
|
||||||
|
configure(options?: { update?: boolean } | null): CacheInterceptor {
|
||||||
|
const instance = new CacheInterceptor(this.httpCacheService);
|
||||||
|
if (options && options.update) {
|
||||||
|
instance.forceUpdate = true;
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
if (request.method !== 'GET') {
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Observable((subscriber: Subscriber<HttpEvent<any>>) => {
|
||||||
|
const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams);
|
||||||
|
if (cachedData !== null) {
|
||||||
|
// Create new response to avoid side-effects
|
||||||
|
subscriber.next(new HttpResponse(cachedData as Object));
|
||||||
|
subscriber.complete();
|
||||||
|
} else {
|
||||||
|
next.handle(request)
|
||||||
|
.subscribe(
|
||||||
|
event => {
|
||||||
|
if (event instanceof HttpResponse) {
|
||||||
|
this.httpCacheService.setCacheData(request.urlWithParams, event);
|
||||||
|
}
|
||||||
|
subscriber.next(event);
|
||||||
|
},
|
||||||
|
error => subscriber.error(error),
|
||||||
|
() => subscriber.complete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||||
|
|
||||||
|
describe('ErrorHandlerInterceptor', () => {
|
||||||
|
let errorHandlerInterceptor: ErrorHandlerInterceptor;
|
||||||
|
let http: HttpClient;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
function createInterceptor() {
|
||||||
|
errorHandlerInterceptor = new ErrorHandlerInterceptor();
|
||||||
|
return errorHandlerInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useFactory: createInterceptor,
|
||||||
|
multi: true
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpClient,
|
||||||
|
HttpTestingController
|
||||||
|
], (_http: HttpClient,
|
||||||
|
_httpMock: HttpTestingController) => {
|
||||||
|
|
||||||
|
http = _http;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should catch error and call error handler', () => {
|
||||||
|
// Arrange
|
||||||
|
// Note: here we spy on private method since target is customization here,
|
||||||
|
// but you should replace it by actual behavior in your app
|
||||||
|
spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
http.get('/toto').subscribe(() => fail('should error'), () => {
|
||||||
|
// Assert
|
||||||
|
expect(ErrorHandlerInterceptor.prototype['errorHandler']).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
httpMock.expectOne({}).flush(null, {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { Logger } from '../logger.service';
|
||||||
|
|
||||||
|
const log = new Logger('ErrorHandlerInterceptor');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a default error handler to all requests.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ErrorHandlerInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
return next.handle(request).pipe(catchError(error => this.errorHandler(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize the default error handler here if needed
|
||||||
|
private errorHandler(response: HttpEvent<any>): Observable<HttpEvent<any>> {
|
||||||
|
if (!environment.production) {
|
||||||
|
// Do something with the error
|
||||||
|
log.error('Request error', response);
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { HttpCacheService, HttpCacheEntry } from './http-cache.service';
|
||||||
|
|
||||||
|
const cachePersistenceKey = 'httpCache';
|
||||||
|
|
||||||
|
describe('HttpCacheService', () => {
|
||||||
|
let httpCacheService: HttpCacheService;
|
||||||
|
let response: HttpResponse<any>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [HttpCacheService]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start fresh
|
||||||
|
window.sessionStorage.removeItem(cachePersistenceKey);
|
||||||
|
window.localStorage.removeItem(cachePersistenceKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([HttpCacheService], (_httpCacheService: HttpCacheService) => {
|
||||||
|
httpCacheService = _httpCacheService;
|
||||||
|
|
||||||
|
response = new HttpResponse({ body: 'data' });
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpCacheService.cleanCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCacheData', () => {
|
||||||
|
it('should set cache data', () => {
|
||||||
|
// Act
|
||||||
|
httpCacheService.setCacheData('/popo', response);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/popo')).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace existing data', () => {
|
||||||
|
// Arrange
|
||||||
|
const newResponse = new HttpResponse({ body: 'new data' });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
httpCacheService.setCacheData('/popo', response);
|
||||||
|
httpCacheService.setCacheData('/popo', newResponse);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/popo')).toEqual(newResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set cache date correctly', () => {
|
||||||
|
// Act
|
||||||
|
const date = new Date(123);
|
||||||
|
httpCacheService.setCacheData('/popo', response, date);
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/popo')).lastUpdated).toBe(date);
|
||||||
|
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/hoho')).lastUpdated).not.toBe(date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCacheData', () => {
|
||||||
|
it('should return null if no cache', () => {
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached data if exists', () => {
|
||||||
|
// Act
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached data with url parameters if exists', () => {
|
||||||
|
// Act
|
||||||
|
httpCacheService.setCacheData('/hoho?pif=paf', response);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/hoho?pif=paf')).toEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHttpCacheEntry', () => {
|
||||||
|
it('should return null if no cache', () => {
|
||||||
|
expect(httpCacheService.getHttpCacheEntry('/hoho')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached data date if exists', () => {
|
||||||
|
// Arrange
|
||||||
|
const date = new Date(123);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
httpCacheService.setCacheData('/hoho', response, date);
|
||||||
|
const entry = httpCacheService.getHttpCacheEntry('/hoho');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(entry).not.toBeNull();
|
||||||
|
expect((<HttpCacheEntry>entry).lastUpdated).toEqual(date);
|
||||||
|
expect((<HttpCacheEntry>entry).data).toEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearCacheData', () => {
|
||||||
|
it('should clear existing cache data', () => {
|
||||||
|
// Set cache
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
httpCacheService.clearCache('/hoho');
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if no cache exists', () => {
|
||||||
|
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
|
||||||
|
httpCacheService.clearCache('/hoho');
|
||||||
|
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanCache', () => {
|
||||||
|
it('should clear all cache if no date is specified', () => {
|
||||||
|
// Set cache
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
httpCacheService.setCacheData('/popo', response);
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||||
|
expect(httpCacheService.getCacheData('/popo')).toBe(response);
|
||||||
|
|
||||||
|
// Clean cache
|
||||||
|
httpCacheService.cleanCache();
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||||
|
expect(httpCacheService.getCacheData('/popo')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear existing since specified date', () => {
|
||||||
|
// Set cache
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||||
|
|
||||||
|
// Clean cache
|
||||||
|
httpCacheService.cleanCache(new Date());
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not affect cache entries newer than specified date', () => {
|
||||||
|
// Set cache
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
|
||||||
|
|
||||||
|
// Clean cache
|
||||||
|
const date = new Date();
|
||||||
|
httpCacheService.setCacheData('/lolo', response, new Date(date.getTime() + 10));
|
||||||
|
httpCacheService.cleanCache(date);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
|
||||||
|
expect(httpCacheService.getCacheData('/lolo')).toBe(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPersistence', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
httpCacheService.setPersistence();
|
||||||
|
httpCacheService.cleanCache = jasmine.createSpy('cleanCache');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear previous cache data when persistence value change', () => {
|
||||||
|
httpCacheService.setPersistence('local');
|
||||||
|
expect(httpCacheService.cleanCache).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist cache to local storage', () => {
|
||||||
|
expect(localStorage.getItem(cachePersistenceKey)).toBeNull();
|
||||||
|
|
||||||
|
httpCacheService.setPersistence('local');
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
|
||||||
|
expect(localStorage.getItem(cachePersistenceKey)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist cache to session storage', () => {
|
||||||
|
expect(sessionStorage.getItem(cachePersistenceKey)).toBeNull();
|
||||||
|
|
||||||
|
httpCacheService.setPersistence('session');
|
||||||
|
httpCacheService.setCacheData('/hoho', response);
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem(cachePersistenceKey)).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpResponse } from '@angular/common/http';
|
||||||
|
import { each } from 'lodash';
|
||||||
|
|
||||||
|
import { Logger } from '../logger.service';
|
||||||
|
|
||||||
|
const log = new Logger('HttpCacheService');
|
||||||
|
const cachePersistenceKey = 'httpCache';
|
||||||
|
|
||||||
|
export interface HttpCacheEntry {
|
||||||
|
lastUpdated: Date;
|
||||||
|
data: HttpResponse<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a cache facility for HTTP requests with configurable persistence policy.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class HttpCacheService {
|
||||||
|
|
||||||
|
private cachedData: { [key: string]: HttpCacheEntry; } = {};
|
||||||
|
private storage: Storage | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadCacheData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cache data for the specified request.
|
||||||
|
* @param {!string} url The request URL.
|
||||||
|
* @param {ResponseOptions} data The received data.
|
||||||
|
* @param {Date=} lastUpdated The cache last update, current date is used if not specified.
|
||||||
|
*/
|
||||||
|
setCacheData(url: string, data: HttpResponse<any>, lastUpdated?: Date) {
|
||||||
|
this.cachedData[url] = {
|
||||||
|
lastUpdated: lastUpdated || new Date(),
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
log.debug(`Cache set for key: "${url}"`);
|
||||||
|
this.saveCacheData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached data for the specified request.
|
||||||
|
* @param {!string} url The request URL.
|
||||||
|
* @return {?ResponseOptions} The cached data or null if no cached data exists for this request.
|
||||||
|
*/
|
||||||
|
getCacheData(url: string): HttpResponse<any> | null {
|
||||||
|
const cacheEntry = this.cachedData[url];
|
||||||
|
|
||||||
|
if (cacheEntry) {
|
||||||
|
log.debug(`Cache hit for key: "${url}"`);
|
||||||
|
return cacheEntry.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached entry for the specified request.
|
||||||
|
* @param {!string} url The request URL.
|
||||||
|
* @return {?HttpCacheEntry} The cache entry or null if no cache entry exists for this request.
|
||||||
|
*/
|
||||||
|
getHttpCacheEntry(url: string): HttpCacheEntry | null {
|
||||||
|
return this.cachedData[url] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the cached entry (if exists) for the specified request.
|
||||||
|
* @param {!string} url The request URL.
|
||||||
|
*/
|
||||||
|
clearCache(url: string): void {
|
||||||
|
delete this.cachedData[url];
|
||||||
|
log.debug(`Cache cleared for key: "${url}"`);
|
||||||
|
this.saveCacheData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans cache entries older than the specified date.
|
||||||
|
* @param {date=} expirationDate The cache expiration date. If no date is specified, all cache is cleared.
|
||||||
|
*/
|
||||||
|
cleanCache(expirationDate?: Date) {
|
||||||
|
if (expirationDate) {
|
||||||
|
each(this.cachedData, (value: HttpCacheEntry, key: string) => {
|
||||||
|
if (expirationDate >= value.lastUpdated) {
|
||||||
|
delete this.cachedData[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.cachedData = {};
|
||||||
|
}
|
||||||
|
this.saveCacheData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cache persistence policy.
|
||||||
|
* Note that changing the cache persistence will also clear the cache from its previous storage.
|
||||||
|
* @param {'local'|'session'=} persistence How the cache should be persisted, it can be either local or session
|
||||||
|
* storage, or if no value is provided it will be only in-memory (default).
|
||||||
|
*/
|
||||||
|
setPersistence(persistence?: 'local' | 'session') {
|
||||||
|
this.cleanCache();
|
||||||
|
this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null;
|
||||||
|
this.loadCacheData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveCacheData() {
|
||||||
|
if (this.storage) {
|
||||||
|
this.storage[cachePersistenceKey] = JSON.stringify(this.cachedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadCacheData() {
|
||||||
|
const data = this.storage ? this.storage[cachePersistenceKey] : null;
|
||||||
|
this.cachedData = data ? JSON.parse(data) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { HttpClient, HttpInterceptor } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { HttpService } from './http.service';
|
||||||
|
import { HttpCacheService } from './http-cache.service';
|
||||||
|
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||||
|
import { CacheInterceptor } from './cache.interceptor';
|
||||||
|
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||||
|
|
||||||
|
describe('HttpService', () => {
|
||||||
|
let httpCacheService: HttpCacheService;
|
||||||
|
let http: HttpClient;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
ErrorHandlerInterceptor,
|
||||||
|
CacheInterceptor,
|
||||||
|
ApiPrefixInterceptor,
|
||||||
|
HttpCacheService,
|
||||||
|
{
|
||||||
|
provide: HttpClient,
|
||||||
|
useClass: HttpService
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpClient,
|
||||||
|
HttpTestingController,
|
||||||
|
HttpCacheService
|
||||||
|
], (_http: HttpClient,
|
||||||
|
_httpMock: HttpTestingController,
|
||||||
|
_httpCacheService: HttpCacheService) => {
|
||||||
|
|
||||||
|
http = _http;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
httpCacheService = _httpCacheService;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpCacheService.cleanCache();
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use error handler, API prefix and no cache by default', () => {
|
||||||
|
// Arrange
|
||||||
|
let interceptors: HttpInterceptor[];
|
||||||
|
const realRequest = http.request;
|
||||||
|
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||||
|
interceptors = this.interceptors;
|
||||||
|
return realRequest.apply(this, arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const request = http.get('/toto');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(http.request).toHaveBeenCalled();
|
||||||
|
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use cache', () => {
|
||||||
|
// Arrange
|
||||||
|
let interceptors: HttpInterceptor[];
|
||||||
|
const realRequest = http.request;
|
||||||
|
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||||
|
interceptors = this.interceptors;
|
||||||
|
return realRequest.apply(this, arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const request = http
|
||||||
|
.cache()
|
||||||
|
.get('/toto');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy();
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip error handler', () => {
|
||||||
|
// Arrange
|
||||||
|
let interceptors: HttpInterceptor[];
|
||||||
|
const realRequest = http.request;
|
||||||
|
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||||
|
interceptors = this.interceptors;
|
||||||
|
return realRequest.apply(this, arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const request = http
|
||||||
|
.skipErrorHandler()
|
||||||
|
.get('/toto');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy();
|
||||||
|
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use API prefix', () => {
|
||||||
|
// Arrange
|
||||||
|
let interceptors: HttpInterceptor[];
|
||||||
|
const realRequest = http.request;
|
||||||
|
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
|
||||||
|
interceptors = this.interceptors;
|
||||||
|
return realRequest.apply(this, arguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const request = http
|
||||||
|
.disableApiPrefix()
|
||||||
|
.get('/toto');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
request.subscribe(() => {
|
||||||
|
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeFalsy();
|
||||||
|
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
|
||||||
|
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush({});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core';
|
||||||
|
import { HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
|
||||||
|
import { CacheInterceptor } from './cache.interceptor';
|
||||||
|
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
|
||||||
|
|
||||||
|
// HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly
|
||||||
|
// (see https://github.com/Microsoft/TypeScript/issues/13897)
|
||||||
|
declare module '@angular/common/http/src/client' {
|
||||||
|
|
||||||
|
// Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of
|
||||||
|
// HttpClient with HttpService using dependency injection
|
||||||
|
export interface HttpClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables caching for this request.
|
||||||
|
* @param {boolean} forceUpdate Forces request to be made and updates cache entry.
|
||||||
|
* @return {HttpClient} The new instance.
|
||||||
|
*/
|
||||||
|
cache(forceUpdate?: boolean): HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips default error handler for this request.
|
||||||
|
* @return {HttpClient} The new instance.
|
||||||
|
*/
|
||||||
|
skipErrorHandler(): HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not use API prefix for this request.
|
||||||
|
* @return {HttpClient} The new instance.
|
||||||
|
*/
|
||||||
|
disableApiPrefix(): HttpClient;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// From @angular/common/http/src/interceptor: allows to chain interceptors
|
||||||
|
class HttpInterceptorHandler implements HttpHandler {
|
||||||
|
|
||||||
|
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { }
|
||||||
|
|
||||||
|
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
|
||||||
|
return this.interceptor.intercept(request, this.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to override default dynamic interceptors that can be disabled with the HttpService extension.
|
||||||
|
* Except for very specific needs, you should better configure these interceptors directly in the constructor below
|
||||||
|
* for better readability.
|
||||||
|
*
|
||||||
|
* For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard
|
||||||
|
* HTTP_INTERCEPTORS token.
|
||||||
|
*/
|
||||||
|
export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken<HttpInterceptor>('HTTP_DYNAMIC_INTERCEPTORS');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends HttpClient with per request configuration using dynamic interceptors.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class HttpService extends HttpClient {
|
||||||
|
|
||||||
|
constructor(private httpHandler: HttpHandler,
|
||||||
|
private injector: Injector,
|
||||||
|
@Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = []) {
|
||||||
|
super(httpHandler);
|
||||||
|
|
||||||
|
if (!this.interceptors) {
|
||||||
|
// Configure default interceptors that can be disabled here
|
||||||
|
this.interceptors = [
|
||||||
|
this.injector.get(ApiPrefixInterceptor),
|
||||||
|
this.injector.get(ErrorHandlerInterceptor)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache(forceUpdate?: boolean): HttpClient {
|
||||||
|
const cacheInterceptor = this.injector.get(CacheInterceptor).configure({ update: forceUpdate });
|
||||||
|
return this.addInterceptor(cacheInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
skipErrorHandler(): HttpClient {
|
||||||
|
return this.removeInterceptor(ErrorHandlerInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
disableApiPrefix(): HttpClient {
|
||||||
|
return this.removeInterceptor(ApiPrefixInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the original method to wire interceptors when triggering the request.
|
||||||
|
request(method?: any, url?: any, options?: any): any {
|
||||||
|
const handler = this.interceptors.reduceRight(
|
||||||
|
(next, interceptor) => new HttpInterceptorHandler(next, interceptor),
|
||||||
|
this.httpHandler
|
||||||
|
);
|
||||||
|
return new HttpClient(handler).request(method, url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeInterceptor(interceptorType: Function): HttpService {
|
||||||
|
return new HttpService(
|
||||||
|
this.httpHandler,
|
||||||
|
this.injector,
|
||||||
|
this.interceptors.filter(i => !(i instanceof interceptorType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addInterceptor(interceptor: HttpInterceptor): HttpService {
|
||||||
|
return new HttpService(
|
||||||
|
this.httpHandler,
|
||||||
|
this.injector,
|
||||||
|
this.interceptors.concat([interceptor])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { extract, I18nService } from './i18n.service';
|
||||||
|
|
||||||
|
const defaultLanguage = 'en-US';
|
||||||
|
const supportedLanguages = ['eo', 'en-US', 'fr-FR'];
|
||||||
|
|
||||||
|
class MockTranslateService {
|
||||||
|
|
||||||
|
currentLang: string;
|
||||||
|
onLangChange = new Subject();
|
||||||
|
|
||||||
|
use(language: string) {
|
||||||
|
this.currentLang = language;
|
||||||
|
this.onLangChange.next({
|
||||||
|
lang: this.currentLang,
|
||||||
|
translations: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowserCultureLang() {
|
||||||
|
return 'en-US';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTranslation(lang: string, translations: Object, shouldMerge?: boolean) { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('I18nService', () => {
|
||||||
|
let i18nService: I18nService;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let onLangChangeSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
I18nService,
|
||||||
|
{ provide: TranslateService, useClass: MockTranslateService },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
I18nService,
|
||||||
|
TranslateService
|
||||||
|
], (_i18nService: I18nService,
|
||||||
|
_translateService: TranslateService) => {
|
||||||
|
|
||||||
|
i18nService = _i18nService;
|
||||||
|
translateService = _translateService;
|
||||||
|
|
||||||
|
// Create spies
|
||||||
|
onLangChangeSpy = jasmine.createSpy('onLangChangeSpy');
|
||||||
|
translateService.onLangChange
|
||||||
|
.subscribe((event: LangChangeEvent) => {
|
||||||
|
onLangChangeSpy(event.lang);
|
||||||
|
});
|
||||||
|
spyOn(translateService, 'use').and.callThrough();
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleanup
|
||||||
|
localStorage.removeItem('language');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extract', () => {
|
||||||
|
it('should not modify string', () => {
|
||||||
|
expect(extract('Hello world !')).toEqual('Hello world !');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
it('should init with default language', () => {
|
||||||
|
// Act
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
|
||||||
|
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init with save language', () => {
|
||||||
|
// Arrange
|
||||||
|
const savedLanguage = 'eo';
|
||||||
|
localStorage.setItem('language', savedLanguage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith(savedLanguage);
|
||||||
|
expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('set language', () => {
|
||||||
|
it('should change current language', () => {
|
||||||
|
// Arrange
|
||||||
|
const newLanguage = 'eo';
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
i18nService.language = newLanguage;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith(newLanguage);
|
||||||
|
expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change current language without a region match', () => {
|
||||||
|
// Arrange
|
||||||
|
const newLanguage = 'fr-CA';
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
i18nService.language = newLanguage;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith('fr-FR');
|
||||||
|
expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change current language to default if unsupported', () => {
|
||||||
|
// Arrange
|
||||||
|
const newLanguage = 'es';
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
i18nService.language = newLanguage;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
|
||||||
|
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get language', () => {
|
||||||
|
it('should return current language', () => {
|
||||||
|
// Arrange
|
||||||
|
i18nService.init(defaultLanguage, supportedLanguages);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const currentLanguage = i18nService.language;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(currentLanguage).toEqual(defaultLanguage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
|
||||||
|
import { includes } from 'lodash';
|
||||||
|
|
||||||
|
import { Logger } from './logger.service';
|
||||||
|
import * as enUS from '../../translations/en-US.json';
|
||||||
|
import * as frFR from '../../translations/fr-FR.json';
|
||||||
|
|
||||||
|
const log = new Logger('I18nService');
|
||||||
|
const languageKey = 'language';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass-through function to mark a string for translation extraction.
|
||||||
|
* Running `npm translations:extract` will include the given string by using this.
|
||||||
|
* @param {string} s The string to extract for translation.
|
||||||
|
* @return {string} The same string.
|
||||||
|
*/
|
||||||
|
export function extract(s: string) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class I18nService {
|
||||||
|
|
||||||
|
defaultLanguage: string;
|
||||||
|
supportedLanguages: string[];
|
||||||
|
|
||||||
|
constructor(private translateService: TranslateService) {
|
||||||
|
// Embed languages to avoid extra HTTP requests
|
||||||
|
translateService.setTranslation('en-US', enUS);
|
||||||
|
translateService.setTranslation('fr-FR', frFR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes i18n for the application.
|
||||||
|
* Loads language from local storage if present, or sets default language.
|
||||||
|
* @param {!string} defaultLanguage The default language to use.
|
||||||
|
* @param {Array.<String>} supportedLanguages The list of supported languages.
|
||||||
|
*/
|
||||||
|
init(defaultLanguage: string, supportedLanguages: string[]) {
|
||||||
|
this.defaultLanguage = defaultLanguage;
|
||||||
|
this.supportedLanguages = supportedLanguages;
|
||||||
|
this.language = '';
|
||||||
|
|
||||||
|
this.translateService.onLangChange
|
||||||
|
.subscribe((event: LangChangeEvent) => { localStorage.setItem(languageKey, event.lang); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current language.
|
||||||
|
* Note: The current language is saved to the local storage.
|
||||||
|
* If no parameter is specified, the language is loaded from local storage (if present).
|
||||||
|
* @param {string} language The IETF language code to set.
|
||||||
|
*/
|
||||||
|
set language(language: string) {
|
||||||
|
language = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang();
|
||||||
|
let isSupportedLanguage = includes(this.supportedLanguages, language);
|
||||||
|
|
||||||
|
// If no exact match is found, search without the region
|
||||||
|
if (language && !isSupportedLanguage) {
|
||||||
|
language = language.split('-')[0];
|
||||||
|
language = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(language)) || '';
|
||||||
|
isSupportedLanguage = Boolean(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if language is not supported
|
||||||
|
if (!isSupportedLanguage) {
|
||||||
|
language = this.defaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`Language set to ${language}`);
|
||||||
|
this.translateService.use(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current language.
|
||||||
|
* @return {string} The current language code.
|
||||||
|
*/
|
||||||
|
get language(): string {
|
||||||
|
return this.translateService.currentLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export * from './core.module';
|
||||||
|
export * from './authentication/authentication.service';
|
||||||
|
export * from './authentication/authentication.service.mock';
|
||||||
|
export * from './authentication/authentication.guard';
|
||||||
|
export * from './i18n.service';
|
||||||
|
export * from './http/http.service';
|
||||||
|
export * from './http/http-cache.service';
|
||||||
|
export * from './http/api-prefix.interceptor';
|
||||||
|
export * from './http/cache.interceptor';
|
||||||
|
export * from './http/error-handler.interceptor';
|
||||||
|
export * from './route.service';
|
||||||
|
export * from './route-reusable-strategy';
|
||||||
|
export * from './logger.service';
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Logger, LogLevel, LogOutput } from './logger.service';
|
||||||
|
|
||||||
|
const logMethods = ['log', 'info', 'warn', 'error'];
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
let savedConsole: Function[];
|
||||||
|
let savedLevel: LogLevel;
|
||||||
|
let savedOutputs: LogOutput[];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
savedConsole = [];
|
||||||
|
logMethods.forEach((m) => {
|
||||||
|
savedConsole[m] = console[m];
|
||||||
|
console[m] = () => {};
|
||||||
|
});
|
||||||
|
savedLevel = Logger.level;
|
||||||
|
savedOutputs = Logger.outputs;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
logMethods.forEach((m) => { console[m] = savedConsole[m]; });
|
||||||
|
Logger.level = savedLevel;
|
||||||
|
Logger.outputs = savedOutputs;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an instance', () => {
|
||||||
|
expect(new Logger()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a new LogOutput and receives log entries', () => {
|
||||||
|
// Arrange
|
||||||
|
const outputSpy = jasmine.createSpy('outputSpy');
|
||||||
|
const log = new Logger('test');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Logger.outputs.push(outputSpy);
|
||||||
|
|
||||||
|
log.debug('d');
|
||||||
|
log.info('i');
|
||||||
|
log.warn('w');
|
||||||
|
log.error('e', { error: true });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(outputSpy).toHaveBeenCalled();
|
||||||
|
expect(outputSpy.calls.count()).toBe(4);
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd');
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i');
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a new LogOutput and receives only production log entries', () => {
|
||||||
|
// Arrange
|
||||||
|
const outputSpy = jasmine.createSpy('outputSpy');
|
||||||
|
const log = new Logger('test');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Logger.outputs.push(outputSpy);
|
||||||
|
Logger.enableProductionMode();
|
||||||
|
|
||||||
|
log.debug('d');
|
||||||
|
log.info('i');
|
||||||
|
log.warn('w');
|
||||||
|
log.error('e', { error: true });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(outputSpy).toHaveBeenCalled();
|
||||||
|
expect(outputSpy.calls.count()).toBe(2);
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
|
||||||
|
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Simple logger system with the possibility of registering custom outputs.
|
||||||
|
*
|
||||||
|
* 4 different log levels are provided, with corresponding methods:
|
||||||
|
* - debug : for debug information
|
||||||
|
* - info : for informative status of the application (success, ...)
|
||||||
|
* - warning : for non-critical errors that do not prevent normal application behavior
|
||||||
|
* - error : for critical errors that prevent normal application behavior
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* ```
|
||||||
|
* import { Logger } from 'app/core/logger.service';
|
||||||
|
*
|
||||||
|
* const log = new Logger('myFile');
|
||||||
|
* ...
|
||||||
|
* log.debug('something happened');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* To disable debug and info logs in production, add this snippet to your root component:
|
||||||
|
* ```
|
||||||
|
* export class AppComponent implements OnInit {
|
||||||
|
* ngOnInit() {
|
||||||
|
* if (environment.production) {
|
||||||
|
* Logger.enableProductionMode();
|
||||||
|
* }
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible log levels.
|
||||||
|
* LogLevel.Off is never emitted and only used with Logger.level property to disable logs.
|
||||||
|
*/
|
||||||
|
export enum LogLevel {
|
||||||
|
Off = 0,
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
Info,
|
||||||
|
Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log output handler function.
|
||||||
|
*/
|
||||||
|
export type LogOutput = (source: string, level: LogLevel, ...objects: any[]) => void;
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current logging level.
|
||||||
|
* Set it to LogLevel.Off to disable logs completely.
|
||||||
|
*/
|
||||||
|
static level = LogLevel.Debug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional log outputs.
|
||||||
|
*/
|
||||||
|
static outputs: LogOutput[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables production mode.
|
||||||
|
* Sets logging level to LogLevel.Warning.
|
||||||
|
*/
|
||||||
|
static enableProductionMode() {
|
||||||
|
Logger.level = LogLevel.Warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private source?: string) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs messages or objects with the debug level.
|
||||||
|
* Works the same as console.log().
|
||||||
|
*/
|
||||||
|
debug(...objects: any[]) {
|
||||||
|
this.log(console.log, LogLevel.Debug, objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs messages or objects with the info level.
|
||||||
|
* Works the same as console.log().
|
||||||
|
*/
|
||||||
|
info(...objects: any[]) {
|
||||||
|
this.log(console.info, LogLevel.Info, objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs messages or objects with the warning level.
|
||||||
|
* Works the same as console.log().
|
||||||
|
*/
|
||||||
|
warn(...objects: any[]) {
|
||||||
|
this.log(console.warn, LogLevel.Warning, objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs messages or objects with the error level.
|
||||||
|
* Works the same as console.log().
|
||||||
|
*/
|
||||||
|
error(...objects: any[]) {
|
||||||
|
this.log(console.error, LogLevel.Error, objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(func: Function, level: LogLevel, objects: any[]) {
|
||||||
|
if (level <= Logger.level) {
|
||||||
|
const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects;
|
||||||
|
func.apply(console, log);
|
||||||
|
Logger.outputs.forEach((output) => output.apply(output, [this.source, level].concat(objects)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A route strategy allowing for explicit route reuse.
|
||||||
|
* Used as a workaround for https://github.com/angular/angular/issues/18374
|
||||||
|
* To reuse a given route, add `data: { reuse: true }` to the route definition.
|
||||||
|
*/
|
||||||
|
export class RouteReusableStrategy extends RouteReuseStrategy {
|
||||||
|
|
||||||
|
public shouldDetach(route: ActivatedRouteSnapshot): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void { }
|
||||||
|
|
||||||
|
public shouldAttach(route: ActivatedRouteSnapshot): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
|
||||||
|
return (future.routeConfig === curr.routeConfig) || future.data.reuse;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthenticationService } from './authentication/authentication.service';
|
||||||
|
import { MockAuthenticationService } from './authentication/authentication.service.mock';
|
||||||
|
import { AuthenticationGuard } from './authentication/authentication.guard';
|
||||||
|
import { ShellComponent } from './shell/shell.component';
|
||||||
|
import { Route } from './route.service';
|
||||||
|
|
||||||
|
describe('Route', () => {
|
||||||
|
let route: Route;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthenticationGuard,
|
||||||
|
{ provide: AuthenticationService, useClass: MockAuthenticationService },
|
||||||
|
Route
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(inject([Route], (_route: Route) => {
|
||||||
|
route = _route;
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('withShell', () => {
|
||||||
|
it('should create routes as children of shell', () => {
|
||||||
|
// Prepare
|
||||||
|
const testRoutes = [{ path: 'test' }];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = Route.withShell(testRoutes);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.path).toBe('');
|
||||||
|
expect(result.children).toBe(testRoutes);
|
||||||
|
expect(result.component).toBe(ShellComponent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Route as ngRoute, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { ShellComponent } from './shell/shell.component';
|
||||||
|
import { AuthenticationGuard } from './authentication/authentication.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides helper methods to create routes.
|
||||||
|
*/
|
||||||
|
export class Route {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates routes using the shell component and authentication.
|
||||||
|
* @param routes The routes to add.
|
||||||
|
* @return {Route} The new route using shell as the base.
|
||||||
|
*/
|
||||||
|
static withShell(routes: Routes): ngRoute {
|
||||||
|
return {
|
||||||
|
path: '',
|
||||||
|
component: ShellComponent,
|
||||||
|
children: routes,
|
||||||
|
canActivate: [AuthenticationGuard],
|
||||||
|
// Reuse ShellComponent instance when navigating between child views
|
||||||
|
data: { reuse: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<!-- <header>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<a class="navbar-brand" href="https://github.com/ngx-rocket" translate>APP_NAME</a>
|
||||||
|
<button class="navbar-toggler" type="button" aria-controls="navbar-menu"
|
||||||
|
aria-label="Toggle navigation" (click)="toggleMenu()" [attr.aria-expanded]="!menuHidden">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div id="navbar-menu" class="collapse navbar-collapse float-xs-none" [ngbCollapse]="menuHidden">
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-item nav-link text-uppercase" routerLink="/home" routerLinkActive="active">
|
||||||
|
<i class="fas fa-home"></i>
|
||||||
|
<span translate>Home</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item nav-link text-uppercase" routerLink="/about" routerLinkActive="active">
|
||||||
|
<i class="fas fa-question-circle"></i>
|
||||||
|
<span translate>About</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-nav ml-auto">
|
||||||
|
<div class="nav-item" ngbDropdown placement="bottom-right">
|
||||||
|
<a id="language-dropdown" class="nav-link" ngbDropdownToggle>{{currentLanguage}}</a>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="language-dropdown">
|
||||||
|
<button class="dropdown-item" *ngFor="let language of languages" (click)="setLanguage(language)">
|
||||||
|
{{language}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" ngbDropdown placement="bottom-right">
|
||||||
|
<a id="user-dropdown" class="nav-link" ngbDropdownToggle>
|
||||||
|
<i class="fas fa-user-circle"></i>
|
||||||
|
</a>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="user-dropdown">
|
||||||
|
<h6 class="dropdown-header">
|
||||||
|
<span translate>Logged in as</span> <b>{{username}}</b></h6>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button class="dropdown-item" (click)="logout()" translate>Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header> -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<a class="navbar-brand" href="#">MifosX</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor02" aria-controls="navbarColor02" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarColor02">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="/clients" routerLinkActive="active">Clients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Groups</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Loans</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form class="form-inline my-2 my-lg-0">
|
||||||
|
<input class="form-control mr-sm-2" placeholder="Search" type="text">
|
||||||
|
<button class="btn btn-secondary my-2 mr-sm-2 my-sm-0" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
<button class="btn btn-secondary my-2 mr-sm-2 my-sm-0" (click)="logout()" type="submit">Logout</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* @import "src/theme/theme-variables";
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.dropdown-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { AuthenticationService } from '../../authentication/authentication.service';
|
||||||
|
import { MockAuthenticationService } from '../../authentication/authentication.service.mock';
|
||||||
|
import { I18nService } from '../../i18n.service';
|
||||||
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
describe('HeaderComponent', () => {
|
||||||
|
let component: HeaderComponent;
|
||||||
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
TranslateModule.forRoot()
|
||||||
|
],
|
||||||
|
declarations: [HeaderComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthenticationService, useClass: MockAuthenticationService },
|
||||||
|
I18nService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthenticationService } from '../../authentication/authentication.service';
|
||||||
|
import { I18nService } from '../../i18n.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
styleUrls: ['./header.component.scss']
|
||||||
|
})
|
||||||
|
export class HeaderComponent implements OnInit {
|
||||||
|
|
||||||
|
menuHidden = true;
|
||||||
|
|
||||||
|
constructor(private router: Router,
|
||||||
|
private authenticationService: AuthenticationService,
|
||||||
|
private i18nService: I18nService) { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
toggleMenu() {
|
||||||
|
this.menuHidden = !this.menuHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(language: string) {
|
||||||
|
this.i18nService.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.authenticationService.logout()
|
||||||
|
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentLanguage(): string {
|
||||||
|
return this.i18nService.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
get languages(): string[] {
|
||||||
|
return this.i18nService.supportedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
get username(): string | null {
|
||||||
|
const credentials = this.authenticationService.credentials;
|
||||||
|
return credentials ? credentials.username : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<app-header></app-header>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { AuthenticationService } from '../authentication/authentication.service';
|
||||||
|
import { MockAuthenticationService } from '../authentication/authentication.service.mock';
|
||||||
|
import { CoreModule } from '../core.module';
|
||||||
|
import { ShellComponent } from './shell.component';
|
||||||
|
|
||||||
|
describe('ShellComponent', () => {
|
||||||
|
let component: ShellComponent;
|
||||||
|
let fixture: ComponentFixture<ShellComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
CoreModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthenticationService, useClass: MockAuthenticationService }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ShellComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shell',
|
||||||
|
templateUrl: './shell.component.html',
|
||||||
|
styleUrls: ['./shell.component.scss']
|
||||||
|
})
|
||||||
|
export class ShellComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { Route, extract } from '@app/core';
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
Route.withShell([
|
||||||
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
|
{ path: 'home', component: HomeComponent, data: { title: extract('Home') } }
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class HomeRoutingModule { }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<div class="text-center">
|
||||||
|
<img class="logo" src="assets/images/mifos_lg-logo.jpg"/>
|
||||||
|
<!-- <app-loader [isLoading]="isLoading"></app-loader>
|
||||||
|
<q [hidden]="isLoading">{{quote}}</q> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.logo {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
q {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
quotes: "« " " »";
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
import { CoreModule } from '@app/core';
|
||||||
|
import { SharedModule } from '@app/shared';
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
import { QuoteService } from './quote.service';
|
||||||
|
|
||||||
|
describe('HomeComponent', () => {
|
||||||
|
let component: HomeComponent;
|
||||||
|
let fixture: ComponentFixture<HomeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CoreModule,
|
||||||
|
SharedModule,
|
||||||
|
HttpClientTestingModule
|
||||||
|
],
|
||||||
|
declarations: [HomeComponent],
|
||||||
|
providers: [QuoteService]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(HomeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { QuoteService } from './quote.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.scss']
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
|
quote: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
|
||||||
|
constructor(private quoteService: QuoteService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.quoteService.getRandomQuote({ category: 'dev' })
|
||||||
|
.pipe(finalize(() => { this.isLoading = false; }))
|
||||||
|
.subscribe((quote: string) => { this.quote = quote; });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CoreModule } from '@app/core';
|
||||||
|
import { SharedModule } from '@app/shared';
|
||||||
|
import { HomeRoutingModule } from './home-routing.module';
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
import { QuoteService } from './quote.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule,
|
||||||
|
CoreModule,
|
||||||
|
SharedModule,
|
||||||
|
HomeRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
HomeComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
QuoteService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class HomeModule { }
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { TestBed, inject, async } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
import { CoreModule, HttpCacheService } from '@app/core';
|
||||||
|
import { QuoteService } from './quote.service';
|
||||||
|
|
||||||
|
describe('QuoteService', () => {
|
||||||
|
let quoteService: QuoteService;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CoreModule,
|
||||||
|
HttpClientTestingModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
HttpCacheService,
|
||||||
|
QuoteService
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([
|
||||||
|
HttpCacheService,
|
||||||
|
QuoteService,
|
||||||
|
HttpTestingController
|
||||||
|
], (htttpCacheService: HttpCacheService,
|
||||||
|
_quoteService: QuoteService,
|
||||||
|
_httpMock: HttpTestingController) => {
|
||||||
|
|
||||||
|
quoteService = _quoteService;
|
||||||
|
httpMock = _httpMock;
|
||||||
|
|
||||||
|
htttpCacheService.cleanCache();
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRandomQuote', () => {
|
||||||
|
it('should return a random Chuck Norris quote', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockQuote = { value: 'a random quote' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
randomQuoteSubscription.subscribe((quote: string) => {
|
||||||
|
expect(quote).toEqual(mockQuote.value);
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush(mockQuote);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a string in case of error', () => {
|
||||||
|
// Act
|
||||||
|
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
randomQuoteSubscription.subscribe((quote: string) => {
|
||||||
|
expect(typeof quote).toEqual('string');
|
||||||
|
expect(quote).toContain('Error');
|
||||||
|
});
|
||||||
|
httpMock.expectOne({}).flush(null, {
|
||||||
|
status: 500,
|
||||||
|
statusText: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { map, catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}`
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RandomQuoteContext {
|
||||||
|
// The quote's category: 'dev', 'explicit'...
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QuoteService {
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) { }
|
||||||
|
|
||||||
|
getRandomQuote(context: RandomQuoteContext): Observable<string> {
|
||||||
|
return this.httpClient
|
||||||
|
.cache()
|
||||||
|
.get(routes.quote(context))
|
||||||
|
.pipe(
|
||||||
|
map((body: any) => body.value),
|
||||||
|
catchError(() => of('Error, could not load joke :-('))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { extract } from '@app/core';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'login', component: LoginComponent, data: { title: extract('Login') } }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class LoginRoutingModule { }
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<div class="login-container bg-light">
|
||||||
|
<div class="login-box">
|
||||||
|
<img class="text-center img-responsive" src="assets/images/mifos-logo-flat.png" style="width:30%">
|
||||||
|
<div>
|
||||||
|
<h6 class="d-inline-block">v{{version}}</h6>
|
||||||
|
<div class="d-inline-block ml-3" ngbDropdown>
|
||||||
|
<button id="language-dropdown" class="btn btn-sm btn-secondary" ngbDropdownToggle>
|
||||||
|
{{currentLanguage}}
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="language-dropdown">
|
||||||
|
<button class="dropdown-item" *ngFor="let language of languages" (click)="setLanguage(language)">
|
||||||
|
{{language}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card col-md-6 mt-3 mx-auto">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title text-center">
|
||||||
|
<i class="far fa-3x fa-user-circle text-muted"></i>
|
||||||
|
</h4>
|
||||||
|
<form (ngSubmit)="login()" [formGroup]="loginForm" novalidate>
|
||||||
|
<div class="alert alert-danger" [hidden]="!error || isLoading" translate>
|
||||||
|
Username or password incorrect.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="d-block">
|
||||||
|
<input type="text" class="form-control" formControlName="username" autocomplete="username"
|
||||||
|
[placeholder]="'Username' | translate"/>
|
||||||
|
<span hidden translate>Username</span>
|
||||||
|
<small [hidden]="loginForm.controls.username.valid || loginForm.controls.username.untouched"
|
||||||
|
class="text-danger" translate>
|
||||||
|
Username is required
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
<label class="d-block">
|
||||||
|
<input type="password" class="form-control" formControlName="password" autocomplete="current-password"
|
||||||
|
[placeholder]="'Password' | translate" required/>
|
||||||
|
<span hidden translate>Password</span>
|
||||||
|
<small [hidden]="loginForm.controls.password.valid || loginForm.controls.password.untouched"
|
||||||
|
class="text-danger" translate>
|
||||||
|
Password is required
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox" class="form-check-input" formControlName="remember"/>
|
||||||
|
<span translate>Remember me</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100" type="submit" [disabled]="loginForm.invalid || isLoading">
|
||||||
|
<i class="fas fa-cog fa-spin" [hidden]="!isLoading"></i>
|
||||||
|
<span translate>Login</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@import "src/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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { CoreModule } from '@app/core';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
RouterTestingModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
ReactiveFormsModule,
|
||||||
|
CoreModule
|
||||||
|
],
|
||||||
|
declarations: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
import { Logger, I18nService, AuthenticationService } from '@app/core';
|
||||||
|
|
||||||
|
const log = new Logger('Login');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss']
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
|
version: string = environment.version;
|
||||||
|
error: string;
|
||||||
|
loginForm: FormGroup;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
constructor(private router: Router,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private authenticationService: AuthenticationService) {
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.authenticationService.login(this.loginForm.value)
|
||||||
|
.pipe(finalize(() => {
|
||||||
|
this.loginForm.markAsPristine();
|
||||||
|
this.isLoading = false;
|
||||||
|
}))
|
||||||
|
.subscribe(credentials => {
|
||||||
|
log.debug(`${credentials.username} successfully logged in`);
|
||||||
|
this.router.navigate(['/'], { replaceUrl: true });
|
||||||
|
}, error => {
|
||||||
|
log.debug(`Login error: ${error}`);
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(language: string) {
|
||||||
|
this.i18nService.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentLanguage(): string {
|
||||||
|
return this.i18nService.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
get languages(): string[] {
|
||||||
|
return this.i18nService.supportedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createForm() {
|
||||||
|
this.loginForm = this.formBuilder.group({
|
||||||
|
username: ['', Validators.required],
|
||||||
|
password: ['', Validators.required],
|
||||||
|
remember: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { LoginRoutingModule } from './login-routing.module';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
NgbModule,
|
||||||
|
LoginRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LoginComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LoginModule { }
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './shared.module';
|
||||||
|
export * from './loader/loader.component';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div [hidden]="!isLoading" class="text-xs-center">
|
||||||
|
<i class="fas fa-cog fa-spin fa-3x"></i> <span>{{message}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.fa {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoaderComponent } from './loader.component';
|
||||||
|
|
||||||
|
describe('LoaderComponent', () => {
|
||||||
|
let component: LoaderComponent;
|
||||||
|
let fixture: ComponentFixture<LoaderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [LoaderComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be visible by default', () => {
|
||||||
|
// Arrange
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
const div = element.querySelectorAll('div')[0];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(div.getAttribute('hidden')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be visible when app is loading', () => {
|
||||||
|
// Arrange
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
const div = element.querySelectorAll('div')[0];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fixture.componentInstance.isLoading = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(div.getAttribute('hidden')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a message by default', () => {
|
||||||
|
// Arrange
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
const span = element.querySelectorAll('span')[0];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(span.innerText).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display specified message', () => {
|
||||||
|
// Arrange
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
const span = element.querySelectorAll('span')[0];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
fixture.componentInstance.message = 'testing';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(span.innerText).toBe('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-loader',
|
||||||
|
templateUrl: './loader.component.html',
|
||||||
|
styleUrls: ['./loader.component.scss']
|
||||||
|
})
|
||||||
|
export class LoaderComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() isLoading = false;
|
||||||
|
@Input() message: string;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { LoaderComponent } from './loader/loader.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LoaderComponent
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
LoaderComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SharedModule { }
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="50px" height="50px" viewBox="0 0 128 128" xml:space="preserve"><path fill="#4f99ed" fill-opacity="1" d="M64.4 16a49 49 0 0 0-50 48 51 51 0 0 0 50 52.2 53 53 0 0 0 54-52c-.7-48-45-55.7-45-55.7s45.3 3.8 49 55.6c.8 32-24.8 59.5-58 60.2-33 .8-61.4-25.7-62-60C1.3 29.8 28.8.6 64.3 0c0 0 8.5 0 8.7 8.4 0 8-8.6 7.6-8.6 7.6z"><animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="800ms" repeatCount="indefinite"></animateTransform></path></svg>
|
||||||
|
After Width: | Height: | Size: 672 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user