├── .browserslistrc ├── .editorconfig ├── .gitignore ├── .htmlhintrc ├── .stylelintrc ├── .vscode ├── launch.json └── settings.json ├── .yo-rc.json ├── README.md ├── angular.json ├── docs ├── analytics.md ├── backend-proxy.md ├── coding-guides │ ├── angular.md │ ├── build-specific-configurations.md │ ├── e2e-tests.md │ ├── html.md │ ├── sass.md │ ├── typescript.md │ └── unit-tests.md ├── corporate-proxy.md ├── i18n.md ├── readme.md ├── routing.md └── updating.md ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── page-objects │ │ ├── app-shared.po.ts │ │ └── shell.po.ts └── tsconfig.json ├── karma.conf.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── proxy.conf.js ├── src ├── app │ ├── @core │ │ ├── auth │ │ │ ├── auth-config.ts │ │ │ ├── auth-guard-with-forced-login.service.ts │ │ │ ├── auth-guard.service.ts │ │ │ ├── auth-module-config.ts │ │ │ ├── auth.service.ts │ │ │ └── role-guard.service.ts │ │ ├── core.module.ts │ │ ├── http │ │ │ ├── api-prefix.interceptor.spec.ts │ │ │ ├── api-prefix.interceptor.ts │ │ │ ├── error-handler.interceptor.spec.ts │ │ │ └── error-handler.interceptor.ts │ │ ├── index.ts │ │ ├── logger.service.spec.ts │ │ ├── logger.service.ts │ │ └── route-reusable-strategy.ts │ ├── @shared │ │ ├── classes │ │ │ ├── data-response-position.ts │ │ │ ├── data-response.ts │ │ │ ├── data-tables-response.ts │ │ │ ├── query-string-parameters.ts │ │ │ └── url-builder.ts │ │ ├── confirmation-dialog │ │ │ ├── confirmation-dialog.component.html │ │ │ ├── confirmation-dialog.component.scss │ │ │ ├── confirmation-dialog.component.spec.ts │ │ │ └── confirmation-dialog.component.ts │ │ ├── index.ts │ │ ├── loader │ │ │ ├── loader.component.html │ │ │ ├── loader.component.scss │ │ │ ├── loader.component.spec.ts │ │ │ └── loader.component.ts │ │ ├── models │ │ │ └── position.ts │ │ ├── shared.module.ts │ │ └── toast │ │ │ ├── toast.component.html │ │ │ ├── toast.component.scss │ │ │ ├── toast.component.spec.ts │ │ │ └── toast.component.ts │ ├── about │ │ ├── about-routing.module.ts │ │ ├── about.component.html │ │ ├── about.component.scss │ │ ├── about.component.spec.ts │ │ ├── about.component.ts │ │ └── about.module.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── config │ │ └── constants.ts │ ├── fallback.component.ts │ ├── features │ │ ├── admin │ │ │ ├── admin-routing.module.ts │ │ │ ├── admin.component.html │ │ │ ├── admin.component.scss │ │ │ ├── admin.component.spec.ts │ │ │ ├── admin.component.ts │ │ │ └── admin.module.ts │ │ ├── employee │ │ │ ├── detail │ │ │ │ ├── detail.component.html │ │ │ │ ├── detail.component.scss │ │ │ │ ├── detail.component.spec.ts │ │ │ │ └── detail.component.ts │ │ │ ├── employee-routing.module.ts │ │ │ ├── employee.component.html │ │ │ ├── employee.component.scss │ │ │ ├── employee.component.spec.ts │ │ │ ├── employee.component.ts │ │ │ └── employee.module.ts │ │ ├── manager │ │ │ ├── manager-routing.module.ts │ │ │ ├── manager.component.html │ │ │ ├── manager.component.scss │ │ │ ├── manager.component.spec.ts │ │ │ ├── manager.component.ts │ │ │ └── manager.module.ts │ │ └── position │ │ │ ├── detail │ │ │ ├── detail.component.html │ │ │ ├── detail.component.scss │ │ │ ├── detail.component.spec.ts │ │ │ └── detail.component.ts │ │ │ ├── position-routing.module.ts │ │ │ ├── position.component.html │ │ │ ├── position.component.scss │ │ │ ├── position.component.spec.ts │ │ │ ├── position.component.ts │ │ │ └── position.module.ts │ ├── home │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.spec.ts │ │ │ └── dashboard.component.ts │ │ ├── home-routing.module.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ ├── home.component.ts │ │ └── home.module.ts │ ├── i18n │ │ ├── i18n.module.ts │ │ ├── i18n.service.spec.ts │ │ ├── i18n.service.ts │ │ ├── index.ts │ │ ├── language-selector.component.html │ │ ├── language-selector.component.scss │ │ ├── language-selector.component.spec.ts │ │ └── language-selector.component.ts │ ├── services │ │ ├── api-endpoints.service.ts │ │ ├── api-http.service.ts │ │ ├── confirmation-dialog.service.ts │ │ └── toast.service.ts │ ├── shell │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.ts │ │ ├── shell.component.html │ │ ├── shell.component.scss │ │ ├── shell.component.spec.ts │ │ ├── shell.component.ts │ │ ├── shell.module.ts │ │ ├── shell.service.spec.ts │ │ └── shell.service.ts │ ├── should-login.component.html │ ├── should-login.component.scss │ └── should-login.component.ts ├── apple-touch-icon.png ├── assets │ ├── icons8-angularjs.svg │ ├── ngx-rocket-logo.png │ └── ngx-rocket-logo@192.png ├── environments │ ├── .env.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.scss ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── robots.txt ├── silent-refresh.html ├── test.ts ├── theme │ ├── theme-variables.scss │ └── theme.scss ├── translations │ └── en-US.json ├── typings.d.ts └── web.config ├── stats.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | max_line_length = 120 12 | 13 | [*.ts] 14 | quote_type = single 15 | 16 | [*.md] 17 | max_line_length = off 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | # See http://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # Compiled output 5 | /dist 6 | /tmp 7 | /out-tsc 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # Dependencies 12 | /node_modules 13 | 14 | # Cordova 15 | /www 16 | /plugins 17 | /platforms 18 | 19 | # Electron 20 | /dist-electron 21 | /dist-packages 22 | /electron.main.js 23 | 24 | # IDEs and editors 25 | .idea/* 26 | !.idea/runConfigurations/ 27 | !.idea/codeStyleSettings.xml 28 | .project 29 | .classpath 30 | .c9/ 31 | *.launch 32 | .settings/ 33 | xcuserdata/ 34 | *.sublime-workspace 35 | 36 | # IDE - VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | 43 | # Maven 44 | /target 45 | /log 46 | 47 | # Misc 48 | /.sass-cache 49 | /connect.lock 50 | /coverage 51 | /libpeerconnection.log 52 | npm-debug.log 53 | yarn-error.log 54 | testem.log 55 | /typings 56 | /reports 57 | /src/translations/template.* 58 | # /src/environments/.env.* 59 | 60 | # System Files 61 | .DS_Store 62 | Thumbs.db 63 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": false, 3 | "attr-lowercase": false, 4 | "attr-value-double-quotes": true, 5 | "tag-pair": true, 6 | "spec-char-escape": true, 7 | "id-unique": false, 8 | "src-not-empty": true, 9 | "attr-no-duplication": true, 10 | "title-require": true, 11 | "tag-self-close": true, 12 | "head-script-disabled": true, 13 | "doctype-html5": true, 14 | "id-class-value": "dash", 15 | "style-disabled": true, 16 | "inline-style-disabled": true, 17 | "inline-script-disabled": true, 18 | "space-tab-mixed-disabled": "true", 19 | "id-class-ad-disabled": true, 20 | "attr-unsafe-chars": true 21 | } 22 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-scss", 5 | "stylelint-config-prettier" 6 | ], 7 | "rules": { 8 | "font-family-name-quotes": "always-where-recommended", 9 | "function-url-quotes": [ 10 | "always", 11 | { 12 | "except": ["empty"] 13 | } 14 | ], 15 | "selector-attribute-quotes": "always", 16 | "string-quotes": "double", 17 | "max-nesting-depth": 3, 18 | "selector-max-compound-selectors": 3, 19 | "selector-max-specificity": "0,3,2", 20 | "declaration-no-important": true, 21 | "at-rule-no-vendor-prefix": true, 22 | "media-feature-name-no-vendor-prefix": true, 23 | "property-no-vendor-prefix": true, 24 | "selector-no-vendor-prefix": true, 25 | "value-no-vendor-prefix": true, 26 | "no-empty-source": null, 27 | "selector-class-pattern": "[a-z-]+", 28 | "selector-id-pattern": "[a-z-]+", 29 | "selector-max-id": 0, 30 | "selector-no-qualifying-type": true, 31 | "selector-max-universal": 0, 32 | "selector-type-no-unknown": [ 33 | true, 34 | { 35 | "ignore": ["custom-elements", "default-namespace"] 36 | } 37 | ], 38 | "selector-pseudo-element-no-unknown": [ 39 | true, 40 | { 41 | "ignorePseudoElements": ["ng-deep"] 42 | } 43 | ], 44 | "unit-allowed-list": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"], 45 | "max-empty-lines": 2, 46 | "max-line-length": 120 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appService.defaultWebAppToDeploy": "/subscriptions/7d4355af-9f71-4123-8a18-aa68dc430bbf/resourceGroups/appsvc_windows_centralus/providers/Microsoft.Web/sites/CATToolkitAngular", 3 | "appService.deploySubpath": "dist", 4 | "cflint.enabled": false 5 | } 6 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-ngx-rocket": { 3 | "version": "9.2.0", 4 | "props": { 5 | "location": "path", 6 | "strict": false, 7 | "skipInstall": false, 8 | "skipQuickstart": false, 9 | "initGit": true, 10 | "usePrefix": true, 11 | "appName": "CATToolkitAngular", 12 | "target": ["web"], 13 | "pwa": true, 14 | "ui": "bootstrap", 15 | "auth": false, 16 | "lazy": true, 17 | "angulartics": false, 18 | "languages": ["en-US"], 19 | "tools": ["prettier", "hads"], 20 | "utility": [], 21 | "deploy": "none", 22 | "projectName": "CATToolkitAngular", 23 | "packageManager": "npm", 24 | "mobile": [], 25 | "desktop": [] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Information 2 | 3 | - [Angular Starter Kit for Fast Prototype](https://medium.com/scrum-and-coke/angular-starter-kit-tutorial-199e51f734bb) blog 4 | - [Starter Kit](https://cat-angular-starterkit.azurewebsites.net/home) demo 5 | 6 | # Repo Information 7 | 8 | This project was generated with [ngX-Rocket](https://github.com/ngx-rocket/generator-ngx-rocket/) 9 | version 9.2.0 then integrated with OIDC JS Library and added boilerplate code for dashboard, pagination, sort, filter, and CRUD. 10 | 11 | # Related Repos 12 | 13 | - [Token Service](https://github.com/workcontrolgit/cat-toolkit-tokenservice-starter) contains the NET CORE 5 C# source code of the Secure Token Service (STS) server. The project architecture is based on IdentityServer4 Admin UI. 14 | - [ApiResources](https://github.com/workcontrolgit/cat-toolkit-apiresources-starter) contains the NET CORE 5 C# source code of the REST API, supporting pagination, sort, filter, and CRUD. The solution was generated using the [TemplateOnionAPI](https://marketplace.visualstudio.com/items?itemName=workcontrol.VSIXTemplateOnionAPI). 15 | 16 | # Getting started 17 | 18 | 1. Go to project folder and install dependencies: 19 | 20 | ```sh 21 | npm install 22 | ``` 23 | 24 | 2. Launch development server, and open `localhost:4200` in your browser: 25 | 26 | ```sh 27 | npm start 28 | ``` 29 | 30 | # Project structure 31 | 32 | ``` 33 | dist/ web app production build 34 | docs/ project docs and coding guides 35 | e2e/ end-to-end tests 36 | src/ project source code 37 | |- app/ app components 38 | | |- core/ core module (singleton services and single-use components) 39 | | |- shared/ shared module (common components, directives and pipes) 40 | | |- app.component.* app root component (shell) 41 | | |- app.module.ts app root module definition 42 | | |- app-routing.module.ts app routes 43 | | +- ... additional modules and components 44 | |- features/ application features 45 | |- assets/ app assets (images, fonts, sounds...) 46 | |- environments/ values for various build environments 47 | |- theme/ app global scss variables and theme 48 | |- translations/ translations files 49 | |- index.html html entry point 50 | |- main.scss global style entry point 51 | |- main.ts app entry point 52 | |- polyfills.ts polyfills needed by Angular 53 | +- test.ts unit tests entry point 54 | reports/ test and coverage reports 55 | proxy.conf.js backend proxy configuration 56 | ``` 57 | 58 | # Main tasks 59 | 60 | Task automation is based on [NPM scripts](https://docs.npmjs.com/misc/scripts). 61 | 62 | | Task | Description | 63 | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | 64 | | `npm start` | Run development server on `http://localhost:4200/` | 65 | | `npm run serve:sw` | Run test server on `http://localhost:4200/` with service worker enabled | 66 | | `npm run build [-- --configuration=production]` | Lint code and build web app for production (with [AOT](https://angular.io/guide/aot-compiler)) in `dist/` folder | 67 | | `npm test` | Run unit tests via [Karma](https://karma-runner.github.io) in watch mode | 68 | | `npm run test:ci` | Lint code and run unit tests once for continuous integration | 69 | | `npm run e2e` | Run e2e tests using [Protractor](http://www.protractortest.org) | 70 | | `npm run lint` | Lint code | 71 | | `npm run translations:extract` | Extract strings from code and templates to `src/app/translations/template.json` | 72 | | `npm run docs` | Display project documentation and coding guides | 73 | | `npm run prettier` | Automatically format all `.ts`, `.js` & `.scss` files | 74 | 75 | When building the application, you can specify the target configuration using the additional flag 76 | `--configuration ` (do not forget to prepend `--` to pass arguments to npm scripts). 77 | 78 | The default build configuration is `prod`. 79 | 80 | ## Development server 81 | 82 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change 83 | any of the source files. 84 | You should not use `ng serve` directly, as it does not use the backend proxy configuration by default. 85 | 86 | ## Code scaffolding 87 | 88 | Run `npm run generate -- component ` to generate a new component. You can also use 89 | `npm run generate -- directive|pipe|service|class|module`. 90 | 91 | If you have installed [angular-cli](https://github.com/angular/angular-cli) globally with `npm install -g @angular/cli`, 92 | you can also use the command `ng generate` directly. 93 | 94 | ## Additional tools 95 | 96 | Tasks are mostly based on the `angular-cli` tool. Use `ng help` to get more help or go check out the 97 | [Angular-CLI README](https://github.com/angular/angular-cli). 98 | 99 | ## Code formatting 100 | 101 | All `.ts`, `.js` & `.scss` files in this project are formatted automatically using [Prettier](https://prettier.io), 102 | and enforced via the `test:ci` script. 103 | 104 | A pre-commit git hook has been configured on this project to automatically format staged files, using 105 | (pretty-quick)[https://github.com/azz/pretty-quick], so you don't have to care for it. 106 | 107 | You can also force code formatting by running the command `npm run prettier`. 108 | 109 | # What's in the box 110 | 111 | The app template is based on [HTML5](http://whatwg.org/html), [TypeScript](http://www.typescriptlang.org) and 112 | [Sass](http://sass-lang.com). The translation files use the common [JSON](http://www.json.org) format. 113 | 114 | #### Tools 115 | 116 | Development, build and quality processes are based on [angular-cli](https://github.com/angular/angular-cli) and 117 | [NPM scripts](https://docs.npmjs.com/misc/scripts), which includes: 118 | 119 | - Optimized build and bundling process with [Webpack](https://webpack.github.io) 120 | - [Development server](https://webpack.github.io/docs/webpack-dev-server.html) with backend proxy and live reload 121 | - Cross-browser CSS with [autoprefixer](https://github.com/postcss/autoprefixer) and 122 | [browserslist](https://github.com/ai/browserslist) 123 | - Asset revisioning for [better cache management](https://webpack.github.io/docs/long-term-caching.html) 124 | - Unit tests using [Jasmine](http://jasmine.github.io) and [Karma](https://karma-runner.github.io) 125 | - End-to-end tests using [Protractor](https://github.com/angular/protractor) 126 | - Static code analysis: [TSLint](https://github.com/palantir/tslint), [Codelyzer](https://github.com/mgechev/codelyzer), 127 | [Stylelint](http://stylelint.io) and [HTMLHint](http://htmlhint.com/) 128 | - Local knowledgebase server using [Hads](https://github.com/sinedied/hads) 129 | - Automatic code formatting with [Prettier](https://prettier.io) 130 | 131 | #### Libraries 132 | 133 | - [Angular](https://angular.io) 134 | - [Bootstrap 4](https://getbootstrap.com) 135 | - [ng-bootsrap](https://ng-bootstrap.github.io/) 136 | - [Font Awesome](http://fontawesome.io) 137 | - [RxJS](http://reactivex.io/rxjs) 138 | - [ngx-translate](https://github.com/ngx-translate/core) 139 | 140 | #### Coding guides 141 | 142 | - [Angular](docs/coding-guides/angular.md) 143 | - [TypeScript](docs/coding-guides/typescript.md) 144 | - [Sass](docs/coding-guides/sass.md) 145 | - [HTML](docs/coding-guides/html.md) 146 | - [Unit tests](docs/coding-guides/unit-tests.md) 147 | - [End-to-end tests](docs/coding-guides/e2e-tests.md) 148 | 149 | #### Other documentation 150 | 151 | - [I18n guide](docs/i18n.md) 152 | - [Working behind a corporate proxy](docs/corporate-proxy.md) 153 | - [Updating dependencies and tools](docs/updating.md) 154 | - [Using a backend proxy for development](docs/backend-proxy.md) 155 | - [Browser routing](docs/routing.md) 156 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "CATToolkitAngular": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/apple-touch-icon.png", 28 | "src/robots.txt", 29 | "src/manifest.webmanifest", 30 | "src/assets", 31 | "src/web.config", 32 | "src/silent-refresh.html" 33 | ], 34 | "styles": ["src/main.scss", "node_modules/datatables.net-dt/css/jquery.dataTables.css"], 35 | "scripts": ["node_modules/jquery/dist/jquery.js", "node_modules/datatables.net/js/jquery.dataTables.js"], 36 | "vendorChunk": true, 37 | "extractLicenses": false, 38 | "buildOptimizer": false, 39 | "sourceMap": true, 40 | "optimization": false, 41 | "namedChunks": true 42 | }, 43 | "configurations": { 44 | "production": { 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | }, 58 | { 59 | "type": "anyComponentStyle", 60 | "maximumWarning": "6kb", 61 | "maximumError": "10kb" 62 | } 63 | ], 64 | "serviceWorker": true, 65 | "fileReplacements": [ 66 | { 67 | "replace": "src/environments/environment.ts", 68 | "with": "src/environments/environment.prod.ts" 69 | } 70 | ] 71 | }, 72 | "ci": { 73 | "progress": false 74 | } 75 | } 76 | }, 77 | "serve": { 78 | "builder": "@angular-devkit/build-angular:dev-server", 79 | "options": { 80 | "browserTarget": "CATToolkitAngular:build" 81 | }, 82 | "configurations": { 83 | "production": { 84 | "browserTarget": "CATToolkitAngular:build:production" 85 | }, 86 | "ci": {} 87 | } 88 | }, 89 | "extract-i18n": { 90 | "builder": "@angular-devkit/build-angular:extract-i18n", 91 | "options": { 92 | "browserTarget": "CATToolkitAngular:build" 93 | } 94 | }, 95 | "test": { 96 | "builder": "@angular-devkit/build-angular:karma", 97 | "options": { 98 | "main": "src/test.ts", 99 | "polyfills": "src/polyfills.ts", 100 | "tsConfig": "tsconfig.spec.json", 101 | "karmaConfig": "karma.conf.js", 102 | "scripts": [], 103 | "styles": ["src/main.scss"], 104 | "assets": [ 105 | "src/favicon.ico", 106 | "src/apple-touch-icon.png", 107 | "src/robots.txt", 108 | "src/manifest.webmanifest", 109 | "src/assets" 110 | ] 111 | }, 112 | "configurations": { 113 | "ci": { 114 | "progress": false, 115 | "watch": false 116 | } 117 | } 118 | }, 119 | "e2e": { 120 | "builder": "@angular-devkit/build-angular:protractor", 121 | "options": { 122 | "protractorConfig": "e2e/protractor.conf.js", 123 | "devServerTarget": "CATToolkitAngular:serve" 124 | }, 125 | "configurations": { 126 | "production": { 127 | "devServerTarget": "CATToolkitAngular:serve:production" 128 | } 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "CATToolkitAngular" 135 | } 136 | -------------------------------------------------------------------------------- /docs/analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | This project does not come with any analytics library. 4 | Should you decide to use one, you may want to consider [Angulartics2](https://github.com/angulartics/angulartics2). 5 | -------------------------------------------------------------------------------- /docs/backend-proxy.md: -------------------------------------------------------------------------------- 1 | # Backend proxy 2 | 3 | Usually when working on a web application you consume data from custom-made APIs. 4 | 5 | To ease development with our development server integrating live reload while keeping your backend API calls working, 6 | we also have setup a backend proxy to redirect API calls to whatever URL and port you want. This allows you: 7 | 8 | - To develop frontend features without the need to run an API backend locally 9 | - To use a local development server without [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issues 10 | - To debug frontend code with data from a remote testing platform directly 11 | 12 | ## How to configure 13 | 14 | In the root folder you will find a `proxy.conf.js`, containing the backend proxy configuration. 15 | 16 | The interesting part is there: 17 | 18 | ```js 19 | const proxyConfig = [ 20 | { 21 | context: '/api', 22 | pathRewrite: { '^/api': '' }, 23 | target: 'http://api.icndb.com', 24 | changeOrigin: true, 25 | }, 26 | ]; 27 | ``` 28 | 29 | This is where you can setup one or more proxy rules. 30 | 31 | For the complete set of options, see the `http-proxy-middleware` 32 | [documentation](https://github.com/chimurai/http-proxy-middleware#options). 33 | 34 | ### Corporate proxy support 35 | 36 | To allow external API calls redirection through a corporate proxy, you will also find a `setupForCorporateProxy()` 37 | function in the proxy configuration file. By default, this method configures a corporate proxy agent based on the 38 | `HTTP_PROXY` environment variable, see the [corporate proxy documentation](corporate-proxy.md) for more details. 39 | 40 | If you need to, you can further customize this function to fit the network of your working environment. 41 | 42 | If your corporate proxy use a custom SSL certificate, your may need to add the `secure: false` option to your 43 | backend proxy configuration. 44 | -------------------------------------------------------------------------------- /docs/coding-guides/angular.md: -------------------------------------------------------------------------------- 1 | # Introduction to Angular and modern design patterns 2 | 3 | [Angular](https://angular.io) (aka Angular 2, 4, 5, 6...) is a new framework completely rewritten from the ground up, 4 | replacing the now well-known [AngularJS](https://angularjs.org) framework (aka Angular 1.x). 5 | 6 | More that just a framework, Angular should now be considered as a whole _platform_ which comes with a complete set of 7 | tools, like its own [CLI](https://github.com/angular/angular-cli), [debug utilities](https://augury.angular.io) or 8 | [performance tools](https://github.com/angular/angular/tree/master/packages/benchpress). 9 | 10 | Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved, 11 | probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can 12 | definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid 13 | frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools, 14 | patterns and documentation. Yes, the Angular learning curve may seems a little steep, but it’s definitely worth it. 15 | 16 | ## Getting started 17 | 18 | #### Newcomer 19 | 20 | If you're new to Angular you may feel overwhelmed by the quantity of new concepts to apprehend, so before digging 21 | into this project you may want to start with [this progressive tutorial](https://angular.io/tutorial) that will guide 22 | you step by step into building a complete Angular application. 23 | 24 | #### AngularJS veteran 25 | 26 | If you come from AngularJS and want to dig straight in the new version, you may want to take a look at the 27 | [AngularJS vs 2 quick reference](https://angular.io/guide/ajs-quick-reference). 28 | 29 | #### Cheatsheet 30 | 31 | Until you know the full Angular API by heart, you may want to keep this 32 | [cheatsheet](https://angular.io/guide/cheatsheet) that resumes the syntax and features on a single page at hand. 33 | 34 | ## Style guide 35 | 36 | This project follows the standard [Angular style guide](https://angular.io/guide/styleguide). 37 | 38 | More that just coding rules, this style guide also gives advices and best practices for a good application architecture 39 | and is an **essential reading** for starters. Reading deeper, you can even find many explanations for some design 40 | choices of the framework. 41 | 42 | ## FAQ 43 | 44 | There is a lot to dig in Angular and some questions frequently bother people. In fact, most of unclear stuff seems to be 45 | related to modules, for example the dreaded 46 | [**"Core vs Shared modules"**](https://angular.io/guide/ngmodule-faq#what-kinds-of-modules-should-i-have-and-how-should-i-use-them) 47 | question. 48 | 49 | The guys at Angular may have noticed that since you can now find 50 | [a nice FAQ on their website](https://angular.io/guide/ngmodule-faq#ngmodule-faqs) answering all the common questions 51 | regarding modules. Don't hesitate to take a look at it, even if you think you are experienced enough with Angular :wink:. 52 | 53 | ## Going deeper 54 | 55 | Even though they are not mandatory, Angular was designed for the use of design patterns you may not be accustomed to, 56 | like [reactive programming](#reactive-programming), [unidirectional data flow](#unidirectional-data-flow) and 57 | [centralized state management](#centralized-state-management). 58 | 59 | These concepts are difficult to resume in a few words, and despite being tightly related to each other they concern 60 | specific parts of an application flow, each being quite deep to learn on its own. 61 | 62 | You will essentially find here a list of good starting points to learn more on these subjects. 63 | 64 | #### Reactive programming 65 | 66 | You may not be aware of it, but Angular is now a _reactive system_ by design. 67 | Although you are not forced to use reactive programming patterns, they make the core of the framework and it is 68 | definitely recommended to learn them if you want to leverage the best of Angular. 69 | 70 | Angular uses [RxJS](http://reactivex.io/rxjs/) to implement the _Observable_ pattern. 71 | 72 | > An _Observable_ is a stream of asynchronous events that can be processed with array-like operators. 73 | 74 | ##### From promises to observables 75 | 76 | While AngularJS used to rely heavily on [_Promises_](https://docs.angularjs.org/api/ng/service/$q) to handle 77 | asynchronous events, _Observables_ are now used instead in Angular. Even though in specific cases like for HTTP 78 | requests, an _Observable_ can be converted into a _Promise_, it is recommended to embrace the new paradigm as it can a 79 | lot more than _Promises_, with way less code. This transition is also explained in the 80 | [Angular tutorial](https://angular.io/tutorial/toh-pt6#!%23observables). 81 | Once you have made the switch, you will never look back again. 82 | 83 | ##### Learning references 84 | 85 | - [What is reactive programming?](http://paulstovell.com/blog/reactive-programming), explained nicely through a simple 86 | imaged story _(5 min)_ 87 | 88 | - [The introduction to reactive programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754), 89 | the title says it all _(30 min)_ 90 | 91 | - [Functional reactive programming for Angular 2 developers](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/), 92 | see the functional reactive programming principles in practice with Angular _(15 min)_ 93 | 94 | - [RxMarbles](http://rxmarbles.com), a graphical representation of Rx operators that greatly help to understand their 95 | usage 96 | 97 | #### Unidirectional data flow 98 | 99 | In opposition with AngularJS where one of its selling points was two-way data binding which ended up causing a lot of 100 | major headaches for complex applications, Angular now enforces unidirectional data flow. 101 | 102 | What does it means? Said with other words, it means that change detection cannot cause cycles, which was one of 103 | AngularJS problematic points. It also helps to maintain simpler and more predictable data flows in applications, along 104 | with substantial performance improvements. 105 | 106 | **Wait, then why the Angular documentation have mention of a 107 | [two-way binding syntax](https://angular.io/guide/template-syntax#binding-syntax-an-overview)?** 108 | 109 | If you look closely, the new two-way binding syntax is just syntactic sugar to combine two _one-way_ bindings (a 110 | _property_ and _event_ binding), keeping the data flow unidirectional. 111 | 112 | This change is really important, as it was often the cause of performance issues with AngularJS, and it one of the 113 | pillars enabling better performance in new Angular apps. 114 | 115 | While Angular tries to stay _pattern-agnostic_ and can be used with conventional MV\* patterns, it was designed with 116 | reactive programming in mind and really shines when used with reactive data flow patterns like 117 | [redux](http://redux.js.org/docs/basics/DataFlow.html), 118 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) or 119 | [MVI](http://futurice.com/blog/reactive-mvc-and-the-virtual-dom). 120 | 121 | #### Centralized state management 122 | 123 | As applications grow in size, keeping track of the all its individual components state and data flows can become 124 | tedious, and tend to be difficult to manage and debug. 125 | 126 | The main goal of using a centralized state management is to make state changes _predictable_ by imposing certain 127 | restrictions on how and when updates can happen, using _unidirectional data flow_. 128 | 129 | This approach was first made popular with React with introduction of the 130 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) architecture. Many libraries emerged then 131 | trying to adapt and refine the original concept, and one of these gained massive popularity by providing a simpler, 132 | elegant alternative: [Redux](http://redux.js.org/docs/basics/DataFlow.html). 133 | 134 | Redux is at the same time a library (with the big _R_) and a design pattern (with the little _r_), the latter being 135 | framework-agnostic and working very well with Angular. 136 | 137 | The _redux_ design pattern is based on these [3 principles](http://redux.js.org/docs/introduction/ThreePrinciples.html): 138 | 139 | - The application state is a _single immutable_ data structure 140 | - A state change is triggered by an _action_, an object describing what happened 141 | - Pure functions called _reducers_ take the previous state and the next action to compute the new state 142 | 143 | The core concepts behind these principles are nicely explained in 144 | [this example](http://redux.js.org/docs/introduction/CoreConcepts.html) _(3 min)_. 145 | 146 | For those interested, the redux pattern was notably inspired by 147 | [The Elm Architecture](https://guide.elm-lang.org/architecture/) and the [CQRS](https://martinfowler.com/bliki/CQRS.html) 148 | pattern. 149 | 150 | ##### Which library to use? 151 | 152 | You can make Angular work with any state management library you like, but your best bet would be to use 153 | [NGXS](http://ngxs.io) or [@ngrx](https://github.com/ngrx/platform). Both works the same as the popular 154 | [Redux](http://redux.js.org) library, but with a tight integration with Angular and [RxJS](http://reactivex.io/rxjs/), 155 | with some nice additional developer utilities. 156 | 157 | NGXS is based on the same concepts as @ngrx, but with less boilerplate and a nicer syntax, making it less intimidating. 158 | 159 | Here are some resources to get started: 160 | 161 | - [Angular NGXS tutorial with example from scratch](https://appdividend.com/2018/07/03/angular-ngxs-tutorial-with-example-from-scratch/), 162 | a guided tutorial for NGXS _(10 min)_ 163 | 164 | - [Build a better Angular 2 application with redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/), 165 | a nice tutorial for @ngrx _(30 min)_ 166 | 167 | - [Comprehensive introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510), an in-depth 168 | walkthrough to this library usage in Angular _(60 min)_ 169 | 170 | ##### When to use it? 171 | 172 | You may have noticed that the starter template does not include a centralized state management system out of the box. 173 | Why is that? Well, while there is many benefits from using this pattern, the choice is ultimately up to your team and 174 | what you want to achieve with your app. 175 | 176 | Keep in mind that using a single centralized state for your app introduces a new layer a complexity 177 | [that might not be needed](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367), depending of your 178 | goal. 179 | 180 | ## Optimizing performance 181 | 182 | While the new Angular version resolves by design most of the performance issues that could be experienced with 183 | AngularJS, there is always room for improvements. Just keep in mind that delivering an app with good performance is 184 | often a matter of common sense and sane development practices. 185 | 186 | Here is [a list of key points](https://github.com/mgechev/angular-performance-checklist) to check for in your app to 187 | make sure you deliver the best experience to your customers. 188 | 189 | After going through the checklist, make sure to also run an audit of your page through 190 | [**Lighthouse**](https://developers.google.com/web/tools/lighthouse/), the latest Google tool that gives you meaningful 191 | insight about your app performance, accessibility, mobile compatibility and more. 192 | 193 | ## Keeping Angular up-to-date 194 | 195 | Angular development is moving fast, and updates to the core libs and tools are pushed regularly. 196 | 197 | Fortunately, the Angular team provides tools to help you follow through the updates: 198 | 199 | - `npm run ng update` allows you to update your app and its dependencies 200 | 201 | - The [Angular update website](https://update.angular.io) guides you through Angular changes and migrations, providing 202 | step by step guides from one version to another. 203 | -------------------------------------------------------------------------------- /docs/coding-guides/e2e-tests.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests coding guide 2 | 3 | End-to-end (E2E for short) tests are meant to test the behavior of your application, from start to finish. 4 | 5 | While unit tests are the first choice for catching bugs and regression on individual components, it is a good idea to 6 | complement them with test cases covering the integration between the individual components, hence the need for E2E 7 | tests. 8 | 9 | These tests use [Protractor](https://github.com/angular/protractor), which is a framework built for Angular on top of 10 | [Selenium](https://github.com/SeleniumHQ/selenium) to control browsers and simulate user inputs. 11 | [Jasmine](http://jasmine.github.io) is used as the base test framework. 12 | 13 | Many of protractor's actions and assertions are asynchronous and return promises. To ensure that test steps are 14 | performed in the intended order, generated projects are set up to use async/await as the flow control mechanism 15 | because of its good readability. See the [Protractor async/await](https://www.protractortest.org/#/async-await) page 16 | for more information and examples on using async/await in tests, and the 17 | [Protractor API guide](https://www.protractortest.org/#/api) to determine which API calls are asynchronous. 18 | 19 | Beware that some examples of protractor tests you'll find on the internet might not be using async/await. Tests like 20 | these that you encounter were using the now-deprecated "selenium promise manager" flow control mechanism, so they 21 | should not be used verbatim. See the [Protractor control flow](https://www.protractortest.org/#/control-flow) page 22 | for more details. 23 | 24 | ## Good practices 25 | 26 | - Avoid whenever possible inter-dependencies between your E2E tests 27 | - Run E2E tests on your continuous integration server against different browsers 28 | - If you use an Agile methodology, cover each user story acceptance factors with an E2E test 29 | 30 | ## Page objects 31 | 32 | E2E tests should follow the _[Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)_ pattern. 33 | 34 | #### What is a page object? 35 | 36 | A page object: 37 | 38 | - Models the objects on a page under test: 39 | - _Properties_ wrap page elements 40 | - _Methods_ wrap code that interacts with the page elements 41 | - Simplifies the test scripts 42 | - Reduces the amount of duplicated code 43 | 44 | If the UI changes, the fix only needs to be applied in one place. 45 | 46 | #### How to define a page object 47 | 48 | ```typescript 49 | // login.po.ts 50 | import { browser, element, by } from 'protractor'; 51 | 52 | export class LoginPage { 53 | emailInput = element(by.css('input[name=^"email"]')); 54 | passwordInput = element(by.css('input[name=^"password"]')); 55 | loginButton = element(by.css('button[(click)^="login"]')); 56 | registerButton = element(by.css('button[(click)^="register"]')); 57 | 58 | async navigateTo() { 59 | await browser.get('/'); 60 | } 61 | 62 | async getGreetingText() { 63 | return await element(by.css('.greeting')).getText(); 64 | } 65 | } 66 | ``` 67 | 68 | #### How to use a page object 69 | 70 | ```typescript 71 | // login.e2e-spec.ts 72 | import { LoginPage } from './login.po'; 73 | 74 | describe('Login', () => { 75 | let page: LoginPage; 76 | 77 | beforeEach(async () => { 78 | page = new LoginPage(); 79 | await page.navigateTo(); 80 | }); 81 | 82 | it('should navigate to the register page when the register button is clicked', async () => { 83 | await page.registerButton.click(); 84 | 85 | expect(await browser.getCurrentUrl()).toContain('/register'); 86 | }); 87 | 88 | it('should allow a user to log in', async () => { 89 | await page.emailInput.sendKeys('test@mail.com'); 90 | await page.passwordInput.sendKeys('abc123'); 91 | await page.loginButton.click(); 92 | 93 | expect(await page.getGreetingText()).toContain('Welcome, Test User'); 94 | }); 95 | }); 96 | ``` 97 | 98 | ## Credits 99 | 100 | Parts of this guide were freely inspired by this 101 | [presentation](https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ). 102 | -------------------------------------------------------------------------------- /docs/coding-guides/html.md: -------------------------------------------------------------------------------- 1 | # HTML coding guide 2 | 3 | ## Naming conventions 4 | 5 | - Everything should be named in `kebab-case` (lowercase words separated with a `-`): tags, attributes, IDs, etc, 6 | **except for everything bound to Angular** such variables, directives or events which should be in `camelCase` 7 | - File names should always be in `kebab-case` 8 | 9 | ## Coding rules 10 | 11 | - Use HTML5 doctype: `` 12 | - Use HTML [semantic elements](https://developer.mozilla.org/docs/Web/HTML/Sections_and_Outlines_of_an_HTML5_document) 13 | - Use double quotes `"` around attribute values in tags 14 | - Use a new line for every block, list, or table element, and indent every such child element 15 | - Clearly Separate structure (HTML) from presentation (CSS) from behavior (JavaScript): 16 | - Never use inline CSS or JavaScript 17 | - Keep any logic out of the HTML 18 | - `type` attribute for stylesheets and script tags should be omitted 19 | 20 | ## Common pitfalls 21 | 22 | - **Block**-type tags cannot be nested inside **inline**-type tags: a `
` tag cannot be nested in a ``. 23 | This rule also applies regarding the `display` value of an element. 24 | - HTML is **not** XML: empty tags cannot be self-closing and will result in improper results 25 | - `
` will be interpreted as a simple `
` without closing tag! 26 | - The only tags that allows self-closing are the one that does not require a closing tag in first place: 27 | these are the void elements that do not not accept content `
`, `
`, ``, ``, ``, `` 28 | (and others). 29 | 30 | ## Templates 31 | 32 | In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in 33 | separate files, when more than 3 lines. 34 | 35 | Only use inline templates sparingly in very simple components with less than 3 lines of HTML. 36 | 37 | ## Enforcement 38 | 39 | Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com). 40 | -------------------------------------------------------------------------------- /docs/coding-guides/sass.md: -------------------------------------------------------------------------------- 1 | # Sass coding guide 2 | 3 | [Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large 4 | projects and keeping it maintainable. 5 | 6 | The main benefits of using Sass over plain CSS are _variables_, _nesting_ and _mixins_, see the 7 | [basics guide](http://sass-lang.com/guide) for more details. 8 | 9 | > Note that this project use the newer, CSS-compatible **SCSS** syntax over the old 10 | > [indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html). 11 | 12 | ## Naming conventions 13 | 14 | - In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`). 15 | - File names should always be in `kebab-case` 16 | 17 | ## Coding rules 18 | 19 | - Use single quotes `'` for strings 20 | - Use this general nesting hierarchy when constructing your styles: 21 | 22 | ```scss 23 | // The base component class acts as the namespace, to avoid naming and style collisions 24 | .my-component { 25 | // Put here all component elements (flat) 26 | .my-element { 27 | // Use a third-level only for modifiers and state variations 28 | &.active { ... } 29 | } 30 | } 31 | ``` 32 | 33 | Note that with 34 | [Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation), 35 | the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions. 36 | 37 | > As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found 38 | > it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps 39 | > keeping everything nicely organized, and more importantly, _scoped_. 40 | 41 | Also keep in mind this general rules: 42 | 43 | - Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible 44 | - No more than **3 levels** of nesting 45 | - No more than **3 qualifiers** 46 | 47 | ## Best practices 48 | 49 | - Use object-oriented CSS (OOCSS): 50 | 51 | - Factorize common code in base class, and extend it, for example: 52 | 53 | ```scss 54 | // Base button class 55 | .btn { ... } 56 | 57 | // Color variation 58 | .btn-warning { ... } 59 | 60 | // Size variation 61 | .btn-small { ... } 62 | ``` 63 | 64 | - Try to name class by semantic, not style nor function for better reusability: 65 | Use `.btn-warning`, not `btn-orange` nor `btn-cancel` 66 | - Avoid undoing style, refactor using common base classes and extensions 67 | 68 | - Keep your style scoped 69 | 70 | - Clearly separate **global** (think _framework_) and **components** style 71 | - Global style should only go in `src/theme/`, never in components 72 | - Avoid style interactions between components, if some style may need to be shared, refactor it as a framework 73 | component in put it in your global theme. 74 | - Avoid using wider selectors than needed: always use classes if you can! 75 | 76 | - Avoid rules multiplication 77 | 78 | - The less CSS the better, factorize rules whenever it's possible 79 | - CSS is code, and like any code frequent refactoring is healthy 80 | 81 | - When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in: 82 | - These ugly hacks should only be **temporary** 83 | - Each hack should be documented with the author name, the problem and hack reason 84 | - Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is 85 | reached. 86 | 87 | ## Pitfalls 88 | 89 | - Never use the `!important` keyword. Ever. 90 | - Never use **inline** style in html, even _just for debugging_ (because we **KNOW** it will end up in your commit) 91 | 92 | ## Browser compatibility 93 | 94 | You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer) 95 | takes care of that part for you during the build process. 96 | You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file. 97 | 98 | ## Enforcement 99 | 100 | Coding rules are enforced in this project with [stylelint](https://stylelint.io). 101 | This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the 102 | [`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse). 103 | -------------------------------------------------------------------------------- /docs/coding-guides/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript coding guide 2 | 3 | [TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web 4 | applications. 5 | 6 | Coding conventions and best practices comes from the 7 | [TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the 8 | [TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html). 9 | In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide). 10 | 11 | ## Naming conventions 12 | 13 | - Use `PascalCase` for types, classes, interfaces, constants and enum values. 14 | - Use `camelCase` for variables, properties and functions 15 | - Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03) 16 | - Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this: 17 | ```typescript 18 | private _foo: string; 19 | get foo() { return this._foo; } // foo is read-only to consumers 20 | ``` 21 | 22 | ## Ordering 23 | 24 | - Within a file, type definitions should come first 25 | - Within a class, these priorities should be respected: 26 | - Properties comes before functions 27 | - Static symbols comes before instance symbols 28 | - Public symbols comes before private symbols 29 | 30 | ## Coding rules 31 | 32 | - Use single quotes `'` for strings 33 | - Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see 34 | [JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)). 35 | The only accepted usage for `==` is when you want to check a value against `null` or `undefined`. 36 | - Use `[]` instead of `Array` constructor 37 | - Use `{}` instead of `Object` constructor 38 | - Always specify types for function parameters and returns (if applicable) 39 | - Do not export types/functions unless you need to share it across multiple components 40 | - Do not introduce new types/values to the global namespace 41 | - Use arrow functions over anonymous function expressions 42 | - Only surround arrow function parameters when necessary. 43 | For example, `(x) => x + x` is wrong but the following are correct: 44 | - `x => x + x` 45 | - `(x, y) => x + y` 46 | - `(x: T, y: T) => x === y` 47 | 48 | ## Definitions 49 | 50 | In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are 51 | located in the `node_modules/@types` folder. 52 | 53 | To manage type definitions, use standard `npm install|update|remove` commands. 54 | 55 | ## Enforcement 56 | 57 | Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint). 58 | Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions. 59 | 60 | ## Learn more 61 | 62 | The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good 63 | reference book for TypeScript (and also open-source). 64 | -------------------------------------------------------------------------------- /docs/coding-guides/unit-tests.md: -------------------------------------------------------------------------------- 1 | # Unit tests coding guide 2 | 3 | The main objective of unit tests is to detect regressions and to help you design software components. A suite of 4 | _good_ unit tests can be _immensely_ valuable for your project and makes it easier to refactor and expand your code. 5 | But keep in mind that a suite of _bad_ unit tests can also be _immensely_ painful, and hurt your development by 6 | inhibiting your ability to refactor or alter your code in any way. 7 | 8 | ## What to test? 9 | 10 | Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models... 11 | Shared directives/components should also be covered by unit tests, if you do not have the time to test every single 12 | component. 13 | 14 | Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests 15 | cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the 16 | integration and interactions with other app components based on real use cases scenarios. 17 | 18 | ## Good practices 19 | 20 | - Name your tests cleanly and consistently 21 | - Do not only test nominal cases, the most important tests are the one covering the edge cases 22 | - Each test should be independent to all the others 23 | - Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase 24 | pointless failures and maintenance workload 25 | - Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app 26 | - Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you 27 | should split your tested module into several more independent modules 28 | - Clearly separate or identify these 3 stages of each unit test (the _3A_): _arrange_, _act_ and _assert_ 29 | - When you fix a bug, add a test case for it to prevent regression 30 | 31 | ## Pitfalls 32 | 33 | - Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can, 34 | but if you can't, at least make sure each test resets the relevant statics before and after your tests. 35 | - Don’t unit-test configuration settings 36 | - Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after 37 | essential features of your code unit are tested**, your can think of improving the coverage. 38 | 39 | ## Unit testing with Angular 40 | 41 | A good starting point for learning is the official 42 | [testing guide](https://angular.io/docs/ts/latest/guide/testing.html). 43 | 44 | But as you will most likely want to go bit further in real world apps, these 45 | [example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to 46 | learn how to cover most common testing use cases. 47 | -------------------------------------------------------------------------------- /docs/corporate-proxy.md: -------------------------------------------------------------------------------- 1 | # Working behind a corporate proxy 2 | 3 | ## Environment 4 | 5 | Most tools (including npm and git) use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to work with a 6 | corporate proxy. 7 | 8 | ### Windows 9 | 10 | In Windows environments, add the `HTTP_PROXY` and `HTTPS_PROXY` system environment variable, with these values: 11 | 12 | - HTTP_PROXY: `http://:@:` 13 | - HTTPS_PROXY: `%HTTP_PROXY%` 14 | 15 | ### Unix 16 | 17 | Add these lines to your `~/.bash_profile` or `~/.profile`: 18 | 19 | ```sh 20 | export HTTP_PROXY="http://:@:" 21 | export HTTPS_PROXY="$HTTP_PROXY" 22 | ``` 23 | 24 | ## Proxy with SSL custom certificate 25 | 26 | Some proxy like **zscaler** use a custom SSL certificate to inspect request, which may cause npm commands to fail. 27 | 28 | To solve this problem, you can disable the `strict-ssl` option in npm. 29 | 30 | ## Proxy exceptions 31 | 32 | If you need to access repositories on your local network that should bypass proxy, set the `NO_PROXY` environment 33 | variable, in the same way as `HTTP_PROXY`: 34 | 35 | ### Windows 36 | 37 | - NO_PROXY: `127.0.0.1, localhost, ` 38 | 39 | ### Unix 40 | 41 | ```sh 42 | export NO_PROXY="127.0.0.1, localhost, " 43 | ``` 44 | 45 | ### Npm 46 | 47 | Run this command in your project directory: 48 | 49 | ```sh 50 | npm set strict-ssl false 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/i18n.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | The internationalization of the application is managed by [ngx-translate](https://github.com/ngx-translate/core). 4 | 5 | ## Adding translatable strings 6 | 7 | ### In HTML templates 8 | 9 | Use the `translate` directive on an HTML element to automatically translate its content: 10 | 11 | ```html 12 | This text will be translated. 13 | ``` 14 | 15 | You can also use the `translate` pipe if needed: 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ### In TypeScript code 22 | 23 | If you need to translate strings in JavaScript code, import the `TranslateService` dependency and use the asynchronous 24 | `get()` method: 25 | 26 | ```typescript 27 | let title; 28 | translateService.get('My page title').subscribe((res: string) => { 29 | title = res; 30 | }); 31 | ``` 32 | 33 | ## Extracting strings to translate 34 | 35 | Once you are ready to translate your app, just run `npm run translations:extract`. 36 | It will create a `template.json` file in the `src/translations` folder. 37 | 38 | You can then use any text or code editor to generate the `.json` files for each of your supported languages, and put 39 | them in the `src/translations` folder. 40 | 41 | Do no forget to edit the files in `src/environment` to add the supported languages of your application. 42 | 43 | ### Marking strings for extraction 44 | 45 | If strings are not directly passed to `translateService` or put in HTML templates, they may be missing from the 46 | extraction process. 47 | 48 | For these cases, you have to use the dummy `marker()` function: 49 | 50 | ```typescript 51 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 52 | 53 | function toBeTranslatedLater() { 54 | return marker('A string to be translated'); 55 | } 56 | ``` 57 | 58 | Strings marked like this will then be properly extracted. 59 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # CATToolkitAngular 2 | 3 | Welcome to the project documentation! 4 | 5 | Use `npm run docs` for easier navigation. 6 | 7 | ## Available documentation 8 | 9 | [[index]] 10 | -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | # Browser routing 2 | 3 | To allow navigation without triggering a server request, Angular now use by default the 4 | [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) 5 | API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the _hashbang_ hack 6 | routing style (like `localhost:4200/#/home/`). 7 | 8 | This change has several consequences you should know of, be sure to read the 9 | [browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully 10 | understand the differences between the two approaches. 11 | 12 | In short: 13 | 14 | - 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) 15 | is required for older browsers. 16 | 17 | - You have the option to perform _server-side rendering_ later if you need to increase your app perceived performance. 18 | 19 | - You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file. 20 | 21 | It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the 22 | default HTML5 routing mode. 23 | 24 | ## Server configuration 25 | 26 | To allow your angular application working properly as a _Single Page Application_ (SPA) and allow bookmarking or 27 | refreshing any page, you need some configuration on your server, otherwise you will be running into troubles. 28 | 29 | > Note that during development, the live reload server already supports SPA mode. 30 | 31 | The basic idea is simply to serve the `index.html` file for every request aimed at your application. 32 | 33 | Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server: 34 | 35 | ```js 36 | // Put this in your `server.js` file, after your other rules (APIs, static files...) 37 | app.get('/*', function (req, res) { 38 | res.sendFile(__dirname + '/index.html'); 39 | }); 40 | ``` 41 | 42 | For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or 43 | [Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform _URL rewriting_. 44 | -------------------------------------------------------------------------------- /docs/updating.md: -------------------------------------------------------------------------------- 1 | # Updating npm dependencies 2 | 3 | - Check outdated packages 4 | 5 | ```sh 6 | npm outdated 7 | ``` 8 | 9 | - Update local packages according to `package.json` 10 | 11 | ```sh 12 | npm update 13 | ``` 14 | 15 | - Upgrade packages manually 16 | 17 | ```sh 18 | npm install --save[-dev] @latest 19 | ``` 20 | 21 | Alternatively, you can use [npm-check](https://github.com/dylang/npm-check) to perform an interactive upgrade: 22 | 23 | ```sh 24 | npm-check -u --skip-unused 25 | ``` 26 | 27 | ## Locking package versions 28 | 29 | Starting from `npm@5` a new `package-lock.json` file is 30 | [automatically generated](https://docs.npmjs.com/files/package-locks) when using `npm install` commands, to ensure a 31 | reproducible dependency tree and avoid unwanted package updates. 32 | 33 | If you use a previous npm version, it is recommended to use [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) to 34 | lock down all your dependencies version: 35 | 36 | ```sh 37 | npm shrinkwrap --dev 38 | ``` 39 | 40 | This will create a file `npm-shrinkwrap.json` alongside your `package.json` files. 41 | 42 | > Do not forget to run shrinkwrap each time you manually update your dependencies! 43 | 44 | # Updating angular-related dependencies 45 | 46 | See the [Angular update website](https://update.angular.io) to guide you through the updating/upgrading steps. 47 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | SELENIUM_PROMISE_MANAGER: false, 9 | allScriptsTimeout: 11000, 10 | specs: ['./src/**/*.e2e-spec.ts'], 11 | capabilities: { 12 | browserName: process.env.PROTRACTOR_BROWSER || 'chrome', 13 | chromeOptions: { 14 | binary: process.env.PROTRACTOR_CHROME_BIN || undefined, 15 | args: process.env.PROTRACTOR_CHROME_ARGS ? JSON.parse(process.env.PROTRACTOR_CHROME_ARGS) : ['lang=en-US'], 16 | prefs: { 17 | intl: { accept_languages: 'en-US' }, 18 | }, 19 | }, 20 | }, 21 | // Only works with Chrome and Firefox 22 | directConnect: true, 23 | baseUrl: 'http://localhost:4200/', 24 | framework: 'jasmine2', 25 | jasmineNodeOpts: { 26 | showColors: true, 27 | defaultTimeoutInterval: 30000, 28 | print: function () {}, 29 | }, 30 | onPrepare() { 31 | require('ts-node').register({ 32 | project: require('path').join(__dirname, './tsconfig.json'), 33 | }); 34 | 35 | // Better console spec reporter 36 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: 'pretty' } })); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, ExpectedConditions as until } from 'protractor'; 2 | import { AppSharedPage } from './page-objects/app-shared.po'; 3 | import { ShellPage } from './page-objects/shell.po'; 4 | 5 | describe('when the app loads', () => { 6 | const app = new AppSharedPage(); 7 | const shell = new ShellPage(); 8 | 9 | beforeAll(async () => { 10 | await app.navigateAndSetLanguage(); 11 | }); 12 | 13 | it('should display the shell page', async () => { 14 | expect(await browser.getCurrentUrl()).toContain('/'); 15 | }); 16 | 17 | describe('and the page loads', () => { 18 | it('should display the hello message', async () => { 19 | await browser.wait(until.visibilityOf(shell.welcomeText), 5000, 'Element taking too long to appear'); 20 | expect(await shell.getParagraphText()).toEqual('Hello world !'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/page-objects/app-shared.po.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Use the Page Object pattern to define the page under test. 3 | * See docs/coding-guide/e2e-tests.md for more info. 4 | */ 5 | 6 | import { browser, element, by } from 'protractor'; 7 | 8 | export class AppSharedPage { 9 | async navigateAndSetLanguage() { 10 | // Forces default language 11 | await this.navigateTo(); 12 | await browser.executeScript(() => localStorage.setItem('language', 'en-US')); 13 | } 14 | 15 | async navigateTo() { 16 | await browser.get('/'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /e2e/src/page-objects/shell.po.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Use the Page Object pattern to define the page under test. 3 | * See docs/coding-guide/e2e-tests.md for more info. 4 | */ 5 | 6 | import { browser, element, by } from 'protractor'; 7 | 8 | export class ShellPage { 9 | welcomeText = element(by.css('app-root h1')); 10 | 11 | getParagraphText() { 12 | return this.welcomeText.getText(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": ["jasmine", "jasminewd2", "node"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | const path = require('path'); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '.', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-junit-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma'), 15 | ], 16 | client: { 17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 18 | captureConsole: Boolean(process.env.KARMA_ENABLE_CONSOLE), 19 | }, 20 | junitReporter: { 21 | outputDir: path.join(__dirname, './reports/junit/'), 22 | outputFile: 'TESTS-xunit.xml', 23 | useBrowserName: false, 24 | suite: '', // Will become the package name attribute in xml testsuite element 25 | }, 26 | coverageIstanbulReporter: { 27 | reports: ['html', 'lcovonly', 'text-summary'], 28 | dir: path.join(__dirname, './reports/coverage'), 29 | fixWebpackSourcePaths: true, 30 | }, 31 | reporters: ['progress', 'junit'], 32 | port: 9876, 33 | colors: true, 34 | // Level of logging, can be: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['ChromeHeadless'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] 10 | } 11 | }, 12 | { 13 | "name": "assets", 14 | "installMode": "lazy", 15 | "updateMode": "prefetch", 16 | "resources": { 17 | "files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CATToolkitAngular", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "ng": "ng", 7 | "build": "npm run write:env -s && ng build --prod", 8 | "start": "npm run write:env -s && ng serve --proxy-config proxy.conf.js", 9 | "serve:sw": "npm run build -s && npx http-server ./dist -p 4200", 10 | "lint": "ng lint && stylelint \"src/**/*.scss\" --syntax scss && htmlhint \"src\" --config .htmlhintrc", 11 | "test": "npm run write:env -s && ng test", 12 | "test:ci": "npm run write:env -s && npm run lint -s && ng test --configuration=ci", 13 | "e2e": "npm run write:env -s && ng e2e", 14 | "translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean --sort", 15 | "docs": "hads ./docs -o", 16 | "write:env": "ngx-scripts env npm_package_version", 17 | "prettier": "prettier --write \"./{src,e2e}/**/*.{ts,js,html,scss}\"", 18 | "prettier:check": "prettier --list-different \"./{src,e2e}/**/*.{ts,js,html,scss}\"", 19 | "postinstall": "npm run prettier -s", 20 | "generate": "ng generate" 21 | }, 22 | "dependencies": { 23 | "@angular/animations": "^13.3.11", 24 | "@angular/common": "^13.3.11", 25 | "@angular/compiler": "^13.3.11", 26 | "@angular/core": "^13.3.11", 27 | "@angular/forms": "^13.3.11", 28 | "@angular/localize": "^13.3.11", 29 | "@angular/platform-browser": "^13.3.11", 30 | "@angular/platform-browser-dynamic": "^13.3.11", 31 | "@angular/router": "^13.3.11", 32 | "@angular/service-worker": "^13.3.11", 33 | "@fortawesome/fontawesome-free": "^5.15.1", 34 | "@ng-bootstrap/ng-bootstrap": "^8.0.0", 35 | "@ngx-translate/core": "^13.0.0", 36 | "@rxweb/reactive-form-validators": "^2.1.3", 37 | "angular-datatables": "^11.1.1", 38 | "angular-oauth2-oidc": "^10.0.3", 39 | "bootstrap": "^4.5.3", 40 | "datatables.net": "^1.10.20", 41 | "datatables.net-dt": "^1.10.20", 42 | "jquery": "^3.4.1", 43 | "ng-http-loader": "^10.0.0", 44 | "rxjs": "^6.6.3", 45 | "tslib": "^2.0.1", 46 | "zone.js": "~0.11.4" 47 | }, 48 | "devDependencies": { 49 | "@angular-devkit/build-angular": "^13.3.8", 50 | "@angular/cli": "~13.3.8", 51 | "@angular/compiler-cli": "^13.3.11", 52 | "@angular/language-service": "^13.3.11", 53 | "@biesbjerg/ngx-translate-extract": "^7.0.3", 54 | "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", 55 | "@ngneat/until-destroy": "^8.0.3", 56 | "@ngx-rocket/scripts": "^5.0.0", 57 | "@types/datatables.net": "^1.10.18", 58 | "@types/jasmine": "^3.6.2", 59 | "@types/jasminewd2": "^2.0.8", 60 | "@types/jquery": "^3.3.33", 61 | "@types/node": "^12.19.12", 62 | "codelyzer": "^6.0.1", 63 | "hads": "^3.0.0", 64 | "htmlhint": "^0.14.2", 65 | "https-proxy-agent": "^5.0.0", 66 | "husky": "^4.3.6", 67 | "jasmine-core": "~3.6.0", 68 | "jasmine-spec-reporter": "~6.0.0", 69 | "karma": "~6.3.2", 70 | "karma-chrome-launcher": "~3.1.0", 71 | "karma-coverage-istanbul-reporter": "~3.0.3", 72 | "karma-jasmine": "~4.0.1", 73 | "karma-jasmine-html-reporter": "^1.5.4", 74 | "karma-junit-reporter": "^2.0.1", 75 | "prettier": "^2.2.1", 76 | "pretty-quick": "^3.1.0", 77 | "protractor": "~7.0.0", 78 | "stylelint": "~13.8.0", 79 | "stylelint-config-prettier": "^8.0.2", 80 | "stylelint-config-recommended-scss": "~4.2.0", 81 | "stylelint-config-standard": "~20.0.0", 82 | "stylelint-scss": "~3.18.0", 83 | "ts-node": "^9.1.1", 84 | "tslint": "~6.1.0", 85 | "tslint-config-prettier": "^1.18.0", 86 | "typescript": "~4.6.4", 87 | "webpack": "^5.43.0", 88 | "webpack-bundle-analyzer": "^4.4.2", 89 | "webpack-cli": "^4.7.2" 90 | }, 91 | "prettier": { 92 | "singleQuote": true, 93 | "overrides": [ 94 | { 95 | "files": "*.scss", 96 | "options": { 97 | "singleQuote": false 98 | } 99 | } 100 | ] 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "pretty-quick --staged" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /proxy.conf.js: -------------------------------------------------------------------------------- 1 | const HttpsProxyAgent = require('https-proxy-agent'); 2 | 3 | /* 4 | * API proxy configuration. 5 | * This allows you to proxy HTTP request like `http.get('/api/stuff')` to another server/port. 6 | * This is especially useful during app development to avoid CORS issues while running a local server. 7 | * For more details and options, see https://angular.io/guide/build#using-corporate-proxy 8 | */ 9 | const proxyConfig = [ 10 | { 11 | context: '/api', 12 | pathRewrite: { '^/api': '' }, 13 | target: 'https://api.chucknorris.io', 14 | changeOrigin: true, 15 | secure: false, 16 | }, 17 | ]; 18 | 19 | /* 20 | * Configures a corporate proxy agent for the API proxy if needed. 21 | */ 22 | function setupForCorporateProxy(proxyConfig) { 23 | if (!Array.isArray(proxyConfig)) { 24 | proxyConfig = [proxyConfig]; 25 | } 26 | 27 | const proxyServer = process.env.http_proxy || process.env.HTTP_PROXY; 28 | let agent = null; 29 | 30 | if (proxyServer) { 31 | console.log(`Using corporate proxy server: ${proxyServer}`); 32 | agent = new HttpsProxyAgent(proxyServer); 33 | proxyConfig.forEach((entry) => { 34 | entry.agent = agent; 35 | }); 36 | } 37 | 38 | return proxyConfig; 39 | } 40 | 41 | module.exports = setupForCorporateProxy(proxyConfig); 42 | -------------------------------------------------------------------------------- /src/app/@core/auth/auth-config.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from 'angular-oauth2-oidc'; 2 | import { environment } from '@env/environment'; 3 | 4 | export const authConfig: AuthConfig = { 5 | issuer: environment.Oidc_Issuer, 6 | clientId: environment.Oidc_ClientId, 7 | responseType: environment.Oidc_responseType, 8 | redirectUri: environment.Oidc_redirectUri, 9 | postLogoutRedirectUri: environment.Oidc_postLogoutRedirectUri, 10 | silentRefreshRedirectUri: environment.Oidc_silentRefreshRedirectUri, 11 | scope: environment.Oidc_scope, 12 | useSilentRefresh: environment.Oidc_useSilentRefresh, 13 | silentRefreshTimeout: environment.Oidc_silentRefreshTimeout, 14 | timeoutFactor: environment.Oidc_timeoutFactor, 15 | sessionChecksEnabled: environment.Oidc_sessionChecksEnabled, 16 | showDebugInformation: environment.Oidc_showDebugInformation, 17 | clearHashAfterLogin: environment.Oidc_clearHashAfterLogin, 18 | nonceStateSeparator: environment.Oidc_nonceStateSeparator, 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/@core/auth/auth-guard-with-forced-login.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { filter, switchMap, tap } from 'rxjs/operators'; 5 | 6 | import { AuthService } from './auth.service'; 7 | 8 | @Injectable() 9 | export class AuthGuardWithForcedLogin implements CanActivate { 10 | constructor(private authService: AuthService) {} 11 | 12 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 13 | return this.authService.isDoneLoading$.pipe( 14 | filter((isDone) => isDone), 15 | switchMap((_) => this.authService.isAuthenticated$), 16 | tap((isAuthenticated) => isAuthenticated || this.authService.login(state.url)) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/@core/auth/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | import { AuthService } from './auth.service'; 7 | import { ToastService } from '@app/services/toast.service'; 8 | 9 | @Injectable() 10 | export class AuthGuard implements CanActivate { 11 | profile: any; 12 | constructor(private toastService: ToastService, private authService: AuthService) {} 13 | 14 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 15 | return this.authService.canActivateProtectedRoutes$.pipe( 16 | tap((canActivateProtectedRoutes: boolean) => { 17 | if (canActivateProtectedRoutes) { 18 | return true; 19 | } 20 | this.showToaster('Access denied', 'Please login to continue access'); 21 | return false; 22 | }) 23 | ); 24 | } 25 | 26 | // ngbmodal service 27 | showToaster(title: string, message: string) { 28 | this.toastService.show(message, { 29 | classname: 'bg-danger text-light', 30 | delay: 2000, 31 | autohide: true, 32 | headertext: title, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/@core/auth/auth-module-config.ts: -------------------------------------------------------------------------------- 1 | import { OAuthModuleConfig } from 'angular-oauth2-oidc'; 2 | import { environment } from '@env/environment'; 3 | 4 | export const authModuleConfig: OAuthModuleConfig = { 5 | resourceServer: { 6 | allowedUrls: [environment.Api_Endpoint, environment.Api_Mock_Endpoint], 7 | sendAccessToken: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/@core/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable brace-style */ 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { Router } from '@angular/router'; 5 | import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc'; 6 | import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs'; 7 | import { filter, map } from 'rxjs/operators'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | private isAuthenticatedSubject$ = new BehaviorSubject(false); 12 | public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); 13 | 14 | private isDoneLoadingSubject$ = new ReplaySubject(); 15 | public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable(); 16 | 17 | /** 18 | * Publishes `true` if and only if (a) all the asynchronous initial 19 | * login calls have completed or errorred, and (b) the user ended up 20 | * being authenticated. 21 | * 22 | * In essence, it combines: 23 | * 24 | * - the latest known state of whether the user is authorized 25 | * - whether the ajax calls for initial log in have all been done 26 | */ 27 | public canActivateProtectedRoutes$: Observable = combineLatest([ 28 | this.isAuthenticated$, 29 | this.isDoneLoading$, 30 | ]).pipe(map((values) => values.every((b) => b))); 31 | 32 | private navigateToLoginPage() { 33 | // TODO: Remember current URL 34 | this.router.navigateByUrl('/should-login'); 35 | } 36 | 37 | constructor(private oauthService: OAuthService, private router: Router) { 38 | // Useful for debugging: 39 | this.oauthService.events.subscribe((event) => { 40 | if (event instanceof OAuthErrorEvent) { 41 | console.error('OAuthErrorEvent Object:', event); 42 | } else { 43 | console.warn('OAuthEvent Object:', event); 44 | } 45 | }); 46 | 47 | // This is tricky, as it might cause race conditions (where access_token is set in another 48 | // tab before everything is said and done there. 49 | // TODO: Improve this setup. See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2 50 | window.addEventListener('storage', (event) => { 51 | // The `key` is `null` if the event was caused by `.clear()` 52 | if (event.key !== 'access_token' && event.key !== null) { 53 | return; 54 | } 55 | 56 | console.warn('Noticed changes to access_token (most likely from another tab), updating isAuthenticated'); 57 | this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); 58 | 59 | if (!this.oauthService.hasValidAccessToken()) { 60 | this.navigateToLoginPage(); 61 | } 62 | }); 63 | 64 | this.oauthService.events.subscribe((_) => { 65 | this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); 66 | }); 67 | 68 | this.oauthService.events 69 | .pipe(filter((e) => ['token_received'].includes(e.type))) 70 | .subscribe((e) => this.oauthService.loadUserProfile()); 71 | 72 | this.oauthService.events 73 | .pipe(filter((e) => ['session_terminated', 'session_error'].includes(e.type))) 74 | .subscribe((e) => this.navigateToLoginPage()); 75 | 76 | this.oauthService.setupAutomaticSilentRefresh(); 77 | } 78 | 79 | public runInitialLoginSequence(): Promise { 80 | if (location.hash) { 81 | console.log('Encountered hash fragment, plotting as table...'); 82 | console.table( 83 | location.hash 84 | .substr(1) 85 | .split('&') 86 | .map((kvp) => kvp.split('=')) 87 | ); 88 | } 89 | 90 | // 0. LOAD CONFIG: 91 | // First we have to check to see how the IdServer is 92 | // currently configured: 93 | return ( 94 | this.oauthService 95 | .loadDiscoveryDocument() 96 | 97 | // For demo purposes, we pretend the previous call was very slow 98 | .then(() => new Promise((resolve) => setTimeout(() => resolve(), 1000))) 99 | 100 | // 1. HASH LOGIN: 101 | // Try to log in via hash fragment after redirect back 102 | // from IdServer from initImplicitFlow: 103 | .then(() => this.oauthService.tryLogin()) 104 | 105 | .then(() => { 106 | if (this.oauthService.hasValidAccessToken()) { 107 | return Promise.resolve(); 108 | } 109 | 110 | // 2. SILENT LOGIN: 111 | // Try to log in via a refresh because then we can prevent 112 | // needing to redirect the user: 113 | return this.oauthService 114 | .silentRefresh() 115 | .then(() => Promise.resolve()) 116 | .catch((result) => { 117 | // Subset of situations from https://openid.net/specs/openid-connect-core-1_0.html#AuthError 118 | // Only the ones where it's reasonably sure that sending the 119 | // user to the IdServer will help. 120 | const errorResponsesRequiringUserInteraction = [ 121 | 'interaction_required', 122 | 'login_required', 123 | 'account_selection_required', 124 | 'consent_required', 125 | ]; 126 | 127 | if (result && result.reason && errorResponsesRequiringUserInteraction.indexOf(result.reason.error) >= 0) { 128 | // 3. ASK FOR LOGIN: 129 | // At this point we know for sure that we have to ask the 130 | // user to log in, so we redirect them to the IdServer to 131 | // enter credentials. 132 | // 133 | // Enable this to ALWAYS force a user to login. 134 | // this.login(); 135 | // 136 | // Instead, we'll now do this: 137 | console.warn('User interaction is needed to log in, we will wait for the user to manually log in.'); 138 | return Promise.resolve(); 139 | } 140 | 141 | // We can't handle the truth, just pass on the problem to the 142 | // next handler. 143 | return Promise.reject(result); 144 | }); 145 | }) 146 | 147 | .then(() => { 148 | this.isDoneLoadingSubject$.next(true); 149 | 150 | // Check for the strings 'undefined' and 'null' just to be sure. Our current 151 | // login(...) should never have this, but in case someone ever calls 152 | // initImplicitFlow(undefined | null) this could happen. 153 | if ( 154 | this.oauthService.state && 155 | this.oauthService.state !== 'undefined' && 156 | this.oauthService.state !== 'null' 157 | ) { 158 | let stateUrl = this.oauthService.state; 159 | if (stateUrl.startsWith('/') === false) { 160 | stateUrl = decodeURIComponent(stateUrl); 161 | } 162 | console.log(`There was state of ${this.oauthService.state}, so we are sending you to: ${stateUrl}`); 163 | this.router.navigateByUrl(stateUrl); 164 | } 165 | }) 166 | .catch(() => this.isDoneLoadingSubject$.next(true)) 167 | ); 168 | } 169 | 170 | public login(targetUrl?: string) { 171 | // Note: before version 9.1.0 of the library you needed to 172 | // call encodeURIComponent on the argument to the method. 173 | this.oauthService.initLoginFlow(targetUrl || this.router.url); 174 | } 175 | 176 | public logout() { 177 | this.oauthService.logOut(); 178 | } 179 | public refresh() { 180 | this.oauthService.silentRefresh(); 181 | } 182 | public hasValidToken() { 183 | return this.oauthService.hasValidAccessToken(); 184 | } 185 | 186 | // These normally won't be exposed from a service like this, but 187 | // for debugging it makes sense. 188 | public get accessToken() { 189 | return this.oauthService.getAccessToken(); 190 | } 191 | public get refreshToken() { 192 | return this.oauthService.getRefreshToken(); 193 | } 194 | public get identityClaims() { 195 | return this.oauthService.getIdentityClaims(); 196 | } 197 | public get idToken() { 198 | return this.oauthService.getIdToken(); 199 | } 200 | public get logoutUrl() { 201 | return this.oauthService.logoutUrl; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/app/@core/auth/role-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './auth.service'; 2 | import { Injectable } from '@angular/core'; 3 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, Route } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { tap, map } from 'rxjs/operators'; 6 | import { ToastService } from '@app/services/toast.service'; 7 | 8 | @Injectable() 9 | export class RoleGuard implements CanActivate { 10 | userProfile: any; 11 | 12 | constructor(private authService: AuthService, private toastService: ToastService) {} 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 14 | return this.authService.canActivateProtectedRoutes$.pipe( 15 | map((canActivateProtectedRoutes: boolean) => { 16 | if (canActivateProtectedRoutes) { 17 | // role check only if route contain data.role 18 | // https://javascript.plainenglish.io/4-ways-to-check-whether-the-property-exists-in-a-javascript-object-20c2d96d8f6e 19 | if (!!route.data.role) { 20 | const routeRoles = route.data.role; 21 | 22 | this.userProfile = this.authService.identityClaims; 23 | if (!!this.userProfile.role) { 24 | const userRoles = this.userProfile.role; 25 | 26 | if (userRoles.includes(routeRoles)) { 27 | // user's roles contains route's role 28 | return true; 29 | } else { 30 | // toaster-display role user needs to have to access this route; 31 | this.showToaster('Access denied', 'You do not have role ' + routeRoles); 32 | } 33 | } 34 | } 35 | } 36 | return false; 37 | }) 38 | ); 39 | } 40 | 41 | // ngbmodal service 42 | showToaster(title: string, message: string) { 43 | this.toastService.show(message, { 44 | classname: 'bg-danger text-light', 45 | delay: 2000, 46 | autohide: true, 47 | headertext: title, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/@core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 4 | import { RouteReuseStrategy, RouterModule } from '@angular/router'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | 7 | import { RouteReusableStrategy } from './route-reusable-strategy'; 8 | import { ApiPrefixInterceptor } from './http/api-prefix.interceptor'; 9 | import { ErrorHandlerInterceptor } from './http/error-handler.interceptor'; 10 | 11 | import { Constants } from '../config/constants'; 12 | 13 | // OIDC Integration 14 | import { AuthConfig, OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc'; 15 | import { authConfig } from './auth/auth-config'; 16 | import { AuthGuardWithForcedLogin } from './auth/auth-guard-with-forced-login.service'; 17 | import { AuthGuard } from './auth/auth-guard.service'; 18 | import { authModuleConfig } from './auth/auth-module-config'; 19 | import { AuthService } from './auth/auth.service'; 20 | import { RoleGuard } from './auth/role-guard.service'; 21 | 22 | // We need a factory since localStorage is not available at AOT build time 23 | // eslint-disable-next-line prefer-arrow/prefer-arrow-functions 24 | export function storageFactory(): OAuthStorage { 25 | return localStorage; 26 | } 27 | 28 | @NgModule({ 29 | imports: [CommonModule, HttpClientModule, OAuthModule.forRoot(), TranslateModule, RouterModule], 30 | providers: [ 31 | AuthService, 32 | AuthGuard, 33 | RoleGuard, 34 | AuthGuardWithForcedLogin, 35 | { 36 | provide: HTTP_INTERCEPTORS, 37 | useClass: ApiPrefixInterceptor, 38 | multi: true, 39 | }, 40 | { 41 | provide: HTTP_INTERCEPTORS, 42 | useClass: ErrorHandlerInterceptor, 43 | multi: true, 44 | }, 45 | { 46 | provide: RouteReuseStrategy, 47 | useClass: RouteReusableStrategy, 48 | }, 49 | Constants, 50 | ], 51 | }) 52 | export class CoreModule { 53 | static forRoot(): ModuleWithProviders { 54 | return { 55 | ngModule: CoreModule, 56 | providers: [ 57 | { provide: AuthConfig, useValue: authConfig }, 58 | { provide: OAuthModuleConfig, useValue: authModuleConfig }, 59 | { provide: OAuthStorage, useFactory: storageFactory }, 60 | ], 61 | }; 62 | } 63 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 64 | // Import guard 65 | if (parentModule) { 66 | throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/@core/http/api-prefix.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { ApiPrefixInterceptor } from './api-prefix.interceptor'; 8 | 9 | describe('ApiPrefixInterceptor', () => { 10 | let http: HttpClient; 11 | let httpMock: HttpTestingController; 12 | 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [HttpClientTestingModule], 16 | providers: [ 17 | { 18 | provide: HTTP_INTERCEPTORS, 19 | useClass: ApiPrefixInterceptor, 20 | multi: true, 21 | }, 22 | ], 23 | }); 24 | 25 | http = TestBed.inject(HttpClient); 26 | httpMock = TestBed.inject(HttpTestingController as Type); 27 | }); 28 | 29 | afterEach(() => { 30 | httpMock.verify(); 31 | }); 32 | 33 | it('should prepend environment.serverUrl to the request url', () => { 34 | // Act 35 | http.get('/toto').subscribe(); 36 | 37 | // Assert 38 | httpMock.expectOne({ url: environment.serverUrl + '/toto' }); 39 | }); 40 | 41 | it('should not prepend environment.serverUrl to request url', () => { 42 | // Act 43 | http.get('hTtPs://domain.com/toto').subscribe(); 44 | 45 | // Assert 46 | httpMock.expectOne({ url: 'hTtPs://domain.com/toto' }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/@core/http/api-prefix.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@env/environment'; 6 | 7 | /** 8 | * Prefixes all requests not starting with `http[s]` with `environment.serverUrl`. 9 | */ 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ApiPrefixInterceptor implements HttpInterceptor { 14 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 15 | if (!/^(http|https):/i.test(request.url)) { 16 | request = request.clone({ url: environment.serverUrl + request.url }); 17 | } 18 | return next.handle(request); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/@core/http/error-handler.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 5 | 6 | import { ErrorHandlerInterceptor } from './error-handler.interceptor'; 7 | 8 | describe('ErrorHandlerInterceptor', () => { 9 | let errorHandlerInterceptor: ErrorHandlerInterceptor; 10 | let http: HttpClient; 11 | let httpMock: HttpTestingController; 12 | 13 | function createInterceptor() { 14 | errorHandlerInterceptor = new ErrorHandlerInterceptor(); 15 | return errorHandlerInterceptor; 16 | } 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [HttpClientTestingModule], 21 | providers: [ 22 | { 23 | provide: HTTP_INTERCEPTORS, 24 | useFactory: createInterceptor, 25 | multi: true, 26 | }, 27 | ], 28 | }); 29 | 30 | http = TestBed.inject(HttpClient); 31 | httpMock = TestBed.inject(HttpTestingController as Type); 32 | }); 33 | 34 | afterEach(() => { 35 | httpMock.verify(); 36 | }); 37 | 38 | it('should catch error and call error handler', () => { 39 | // Arrange 40 | // Note: here we spy on private method since target is customization here, 41 | // but you should replace it by actual behavior in your app 42 | spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough(); 43 | 44 | // Act 45 | http.get('/toto').subscribe( 46 | () => fail('should error'), 47 | () => { 48 | // Assert 49 | expect((ErrorHandlerInterceptor.prototype as any).errorHandler).toHaveBeenCalled(); 50 | } 51 | ); 52 | 53 | httpMock.expectOne({}).flush(null, { 54 | status: 404, 55 | statusText: 'error', 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/app/@core/http/error-handler.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { Logger } from '../logger.service'; 8 | 9 | const log = new Logger('ErrorHandlerInterceptor'); 10 | 11 | /** 12 | * Adds a default error handler to all requests. 13 | */ 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class ErrorHandlerInterceptor implements HttpInterceptor { 18 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 19 | return next.handle(request).pipe(catchError((error) => this.errorHandler(error))); 20 | } 21 | 22 | // Customize the default error handler here if needed 23 | private errorHandler(response: HttpEvent): Observable> { 24 | if (!environment.production) { 25 | // Do something with the error 26 | log.error('Request error', response); 27 | } 28 | throw response; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/@core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core.module'; 2 | export * from './http/api-prefix.interceptor'; 3 | export * from './http/error-handler.interceptor'; 4 | export * from './route-reusable-strategy'; 5 | export * from './logger.service'; 6 | export * from '@ngneat/until-destroy'; 7 | -------------------------------------------------------------------------------- /src/app/@core/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel, LogOutput } from './logger.service'; 2 | 3 | const logMethods = ['log', 'info', 'warn', 'error']; 4 | 5 | describe('Logger', () => { 6 | let savedConsole: any[]; 7 | let savedLevel: LogLevel; 8 | let savedOutputs: LogOutput[]; 9 | 10 | beforeAll(() => { 11 | savedConsole = []; 12 | logMethods.forEach((m) => { 13 | savedConsole[m] = console[m]; 14 | console[m] = () => {}; 15 | }); 16 | savedLevel = Logger.level; 17 | savedOutputs = Logger.outputs; 18 | }); 19 | 20 | beforeEach(() => { 21 | Logger.level = LogLevel.Debug; 22 | }); 23 | 24 | afterAll(() => { 25 | logMethods.forEach((m) => { 26 | console[m] = savedConsole[m]; 27 | }); 28 | Logger.level = savedLevel; 29 | Logger.outputs = savedOutputs; 30 | }); 31 | 32 | it('should create an instance', () => { 33 | expect(new Logger()).toBeTruthy(); 34 | }); 35 | 36 | it('should add a new LogOutput and receives log entries', () => { 37 | // Arrange 38 | const outputSpy = jasmine.createSpy('outputSpy'); 39 | const log = new Logger('test'); 40 | 41 | // Act 42 | Logger.outputs.push(outputSpy); 43 | 44 | log.debug('d'); 45 | log.info('i'); 46 | log.warn('w'); 47 | log.error('e', { error: true }); 48 | 49 | // Assert 50 | expect(outputSpy).toHaveBeenCalled(); 51 | expect(outputSpy.calls.count()).toBe(4); 52 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd'); 53 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i'); 54 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 55 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 56 | }); 57 | 58 | it('should add a new LogOutput and receives only production log entries', () => { 59 | // Arrange 60 | const outputSpy = jasmine.createSpy('outputSpy'); 61 | const log = new Logger('test'); 62 | 63 | // Act 64 | Logger.outputs.push(outputSpy); 65 | Logger.enableProductionMode(); 66 | 67 | log.debug('d'); 68 | log.info('i'); 69 | log.warn('w'); 70 | log.error('e', { error: true }); 71 | 72 | // Assert 73 | expect(outputSpy).toHaveBeenCalled(); 74 | expect(outputSpy.calls.count()).toBe(2); 75 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 76 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/app/@core/logger.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple logger system with the possibility of registering custom outputs. 3 | * 4 | * 4 different log levels are provided, with corresponding methods: 5 | * - debug : for debug information 6 | * - info : for informative status of the application (success, ...) 7 | * - warning : for non-critical errors that do not prevent normal application behavior 8 | * - error : for critical errors that prevent normal application behavior 9 | * 10 | * Example usage: 11 | * ``` 12 | * import { Logger } from 'app/core/logger.service'; 13 | * 14 | * const log = new Logger('myFile'); 15 | * ... 16 | * log.debug('something happened'); 17 | * ``` 18 | * 19 | * To disable debug and info logs in production, add this snippet to your root component: 20 | * ``` 21 | * export class AppComponent implements OnInit { 22 | * ngOnInit() { 23 | * if (environment.production) { 24 | * Logger.enableProductionMode(); 25 | * } 26 | * ... 27 | * } 28 | * } 29 | * 30 | * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs. 31 | */ 32 | 33 | /** 34 | * The possible log levels. 35 | * LogLevel.Off is never emitted and only used with Logger.level property to disable logs. 36 | */ 37 | export enum LogLevel { 38 | Off = 0, 39 | Error, 40 | Warning, 41 | Info, 42 | Debug, 43 | } 44 | 45 | /** 46 | * Log output handler function. 47 | */ 48 | export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void; 49 | 50 | export class Logger { 51 | /** 52 | * Current logging level. 53 | * Set it to LogLevel.Off to disable logs completely. 54 | */ 55 | static level = LogLevel.Debug; 56 | 57 | /** 58 | * Additional log outputs. 59 | */ 60 | static outputs: LogOutput[] = []; 61 | 62 | /** 63 | * Enables production mode. 64 | * Sets logging level to LogLevel.Warning. 65 | */ 66 | static enableProductionMode() { 67 | Logger.level = LogLevel.Warning; 68 | } 69 | 70 | constructor(private source?: string) {} 71 | 72 | /** 73 | * Logs messages or objects with the debug level. 74 | * Works the same as console.log(). 75 | */ 76 | debug(...objects: any[]) { 77 | this.log(console.log, LogLevel.Debug, objects); 78 | } 79 | 80 | /** 81 | * Logs messages or objects with the info level. 82 | * Works the same as console.log(). 83 | */ 84 | info(...objects: any[]) { 85 | this.log(console.info, LogLevel.Info, objects); 86 | } 87 | 88 | /** 89 | * Logs messages or objects with the warning level. 90 | * Works the same as console.log(). 91 | */ 92 | warn(...objects: any[]) { 93 | this.log(console.warn, LogLevel.Warning, objects); 94 | } 95 | 96 | /** 97 | * Logs messages or objects with the error level. 98 | * Works the same as console.log(). 99 | */ 100 | error(...objects: any[]) { 101 | this.log(console.error, LogLevel.Error, objects); 102 | } 103 | 104 | private log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) { 105 | if (level <= Logger.level) { 106 | const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects; 107 | func.apply(console, log); 108 | Logger.outputs.forEach((output) => output.apply(output, [this.source, level, ...objects])); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/app/@core/route-reusable-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | /** 5 | * A route strategy allowing for explicit route reuse. 6 | * Used as a workaround for https://github.com/angular/angular/issues/18374 7 | * To reuse a given route, add `data: { reuse: true }` to the route definition. 8 | */ 9 | @Injectable() 10 | export class RouteReusableStrategy extends RouteReuseStrategy { 11 | public shouldDetach(route: ActivatedRouteSnapshot): boolean { 12 | return false; 13 | } 14 | 15 | public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void {} 16 | 17 | public shouldAttach(route: ActivatedRouteSnapshot): boolean { 18 | return false; 19 | } 20 | 21 | public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { 22 | return null; 23 | } 24 | 25 | public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 26 | // Reuse the route if the RouteConfig is the same, or if both routes use the 27 | // same component, because the latter can have different RouteConfigs. 28 | return ( 29 | future.routeConfig === curr.routeConfig || 30 | Boolean(future.routeConfig?.component && future.routeConfig?.component === curr.routeConfig?.component) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/@shared/classes/data-response-position.ts: -------------------------------------------------------------------------------- 1 | import { Position } from '@shared/models/position'; 2 | 3 | export interface DataResponsePosition { 4 | succeeded: boolean; 5 | message: string; 6 | errors: string; 7 | data: Position; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/@shared/classes/data-response.ts: -------------------------------------------------------------------------------- 1 | export interface DataResponse { 2 | succeeded: boolean; 3 | message: string; 4 | errors: string; 5 | data: any[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/@shared/classes/data-tables-response.ts: -------------------------------------------------------------------------------- 1 | export interface DataTablesResponse { 2 | data: any[]; 3 | draw: number; 4 | recordsFiltered: number; 5 | recordsTotal: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/@shared/classes/query-string-parameters.ts: -------------------------------------------------------------------------------- 1 | export class QueryStringParameters { 2 | private paramsAndValues: string[]; 3 | 4 | constructor() { 5 | this.paramsAndValues = []; 6 | } 7 | 8 | public push(key: string, value: Object): void { 9 | value = encodeURIComponent(value.toString()); 10 | this.paramsAndValues.push([key, value].join('=')); 11 | } 12 | 13 | public toString = (): string => this.paramsAndValues.join('&'); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/@shared/classes/url-builder.ts: -------------------------------------------------------------------------------- 1 | // Application Classes 2 | import { QueryStringParameters } from './query-string-parameters'; 3 | 4 | export class UrlBuilder { 5 | public url: string; 6 | public queryString: QueryStringParameters; 7 | 8 | constructor(private baseUrl: string, private action: string, queryString?: QueryStringParameters) { 9 | this.url = [baseUrl, action].join('/'); 10 | this.queryString = queryString || new QueryStringParameters(); 11 | } 12 | 13 | public toString(): string { 14 | const qs: string = this.queryString ? this.queryString.toString() : ''; 15 | return qs ? `${this.url}?${qs}` : this.url; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/@shared/confirmation-dialog/confirmation-dialog.component.html: -------------------------------------------------------------------------------- 1 | 7 | 10 | 14 | -------------------------------------------------------------------------------- /src/app/@shared/confirmation-dialog/confirmation-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/@shared/confirmation-dialog/confirmation-dialog.component.scss -------------------------------------------------------------------------------- /src/app/@shared/confirmation-dialog/confirmation-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfirmationDialogComponent } from './confirmation-dialog.component'; 4 | 5 | describe('ConfirmationDialogComponent', () => { 6 | let component: ConfirmationDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ConfirmationDialogComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ConfirmationDialogComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/@shared/confirmation-dialog/confirmation-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | 4 | @Component({ 5 | selector: 'app-confirmation-dialog', 6 | templateUrl: './confirmation-dialog.component.html', 7 | styleUrls: ['./confirmation-dialog.component.scss'], 8 | }) 9 | export class ConfirmationDialogComponent implements OnInit { 10 | @Input() title: string; 11 | @Input() message: string; 12 | @Input() btnOkText: string; 13 | @Input() btnCancelText: string; 14 | constructor(public activeModal: NgbActiveModal) {} 15 | 16 | ngOnInit(): void {} 17 | 18 | public decline() { 19 | this.activeModal.close(false); 20 | } 21 | public accept() { 22 | this.activeModal.close(true); 23 | } 24 | public dismiss() { 25 | this.activeModal.dismiss(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/@shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shared.module'; 2 | export * from './loader/loader.component'; 3 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ message }} 3 |
4 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.scss: -------------------------------------------------------------------------------- 1 | .fa { 2 | vertical-align: middle; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoaderComponent } from './loader.component'; 4 | 5 | describe('LoaderComponent', () => { 6 | let component: LoaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [LoaderComponent], 13 | }).compileComponents(); 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(LoaderComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should not be visible by default', () => { 24 | // Arrange 25 | const element = fixture.nativeElement; 26 | const div = element.querySelectorAll('div')[0]; 27 | 28 | // Assert 29 | expect(div.getAttribute('hidden')).not.toBeNull(); 30 | }); 31 | 32 | it('should be visible when app is loading', () => { 33 | // Arrange 34 | const element = fixture.nativeElement; 35 | const div = element.querySelectorAll('div')[0]; 36 | 37 | // Act 38 | fixture.componentInstance.isLoading = true; 39 | fixture.detectChanges(); 40 | 41 | // Assert 42 | expect(div.getAttribute('hidden')).toBeNull(); 43 | }); 44 | 45 | it('should not display a message by default', () => { 46 | // Arrange 47 | const element = fixture.nativeElement; 48 | const span = element.querySelectorAll('span')[0]; 49 | 50 | // Assert 51 | expect(span.textContent).toBe(''); 52 | }); 53 | 54 | it('should display specified message', () => { 55 | // Arrange 56 | const element = fixture.nativeElement; 57 | const span = element.querySelectorAll('span')[0]; 58 | 59 | // Act 60 | fixture.componentInstance.message = 'testing'; 61 | fixture.detectChanges(); 62 | 63 | // Assert 64 | expect(span.textContent).toBe('testing'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loader', 5 | templateUrl: './loader.component.html', 6 | styleUrls: ['./loader.component.scss'], 7 | }) 8 | export class LoaderComponent implements OnInit { 9 | @Input() isLoading = false; 10 | @Input() message: string | undefined; 11 | 12 | constructor() {} 13 | 14 | ngOnInit() {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/@shared/models/position.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | id: string; 3 | positionNumber: string; 4 | positionTitle: string; 5 | positionDescription: string; 6 | positionSalary: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/@shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 4 | import { LoaderComponent } from './loader/loader.component'; 5 | import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component'; 6 | import { ToastComponent } from './toast/toast.component'; 7 | @NgModule({ 8 | imports: [CommonModule, NgbModule], 9 | declarations: [LoaderComponent, ConfirmationDialogComponent, ToastComponent], 10 | exports: [LoaderComponent, ConfirmationDialogComponent, ToastComponent], 11 | providers: [], 12 | }) 13 | export class SharedModule {} 14 | -------------------------------------------------------------------------------- /src/app/@shared/toast/toast.component.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | {{ toast.textOrTpl }} 14 | 15 | -------------------------------------------------------------------------------- /src/app/@shared/toast/toast.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/@shared/toast/toast.component.scss -------------------------------------------------------------------------------- /src/app/@shared/toast/toast.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToastComponent } from './toast.component'; 4 | 5 | describe('ToastComponent', () => { 6 | let component: ToastComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ToastComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ToastComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/@shared/toast/toast.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, TemplateRef } from '@angular/core'; 2 | import { ToastService } from '@app/services/toast.service'; 3 | 4 | @Component({ 5 | selector: 'app-toasts', 6 | templateUrl: './toast.component.html', 7 | styleUrls: ['./toast.component.scss'], 8 | host: { '[class.ngb-toasts]': 'true' }, 9 | }) 10 | export class ToastComponent { 11 | constructor(public toastService: ToastService) {} 12 | 13 | isTemplate(toast: any) { 14 | return toast.textOrTpl instanceof TemplateRef; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 4 | 5 | import { AboutComponent } from './about.component'; 6 | 7 | const routes: Routes = [ 8 | // Module is lazy loaded, see app-routing.module.ts 9 | { path: '', component: AboutComponent, data: { title: marker('About') } }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | providers: [], 16 | }) 17 | export class AboutRoutingModule {} 18 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | APP_NAME 5 |

6 |

Version {{ version }}

7 | 8 |

9 | Github Repo: 10 | https://github.com/workcontrolgit/cat-toolkit-angular-starter 13 |

14 |
15 | 16 | 17 | 18 | 19 | {{ this.accessToken }} 20 | 21 | 22 | 23 | Id Token 24 | 25 | {{ this.idToken }} 26 | 27 | 28 | 29 | 30 | {{ this.identityClaims }} 31 | 32 | 33 | 34 | 35 | {{ this.role }} 36 | 37 | 38 | we 39 | 40 |
41 | -------------------------------------------------------------------------------- /src/app/about/about.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/about/about.component.scss -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [AboutComponent], 13 | }).compileComponents(); 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(AboutComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { environment } from '@env/environment'; 4 | import { AuthService } from '@app/@core/auth/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-about', 8 | templateUrl: './about.component.html', 9 | styleUrls: ['./about.component.scss'], 10 | }) 11 | export class AboutComponent implements OnInit { 12 | version: string | null = environment.version; 13 | accessToken: string; 14 | idToken: string; 15 | identityClaims: any; 16 | profile: any; 17 | role: any; 18 | 19 | constructor(private authService: AuthService) {} 20 | 21 | ngOnInit() { 22 | this.accessToken = this.authService.accessToken; 23 | this.idToken = this.authService.idToken; 24 | this.profile = this.authService.identityClaims; 25 | if (this.profile != null && this.profile != undefined) { 26 | this.identityClaims = JSON.stringify(this.profile); 27 | this.role = JSON.stringify(this.profile.role); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { AboutRoutingModule } from './about-routing.module'; 6 | import { AboutComponent } from './about.component'; 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, TranslateModule, AboutRoutingModule, NgbModule], 11 | declarations: [AboutComponent], 12 | }) 13 | export class AboutModule {} 14 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; 3 | import { Shell } from '@app/shell/shell.service'; 4 | import { ShouldLoginComponent } from './should-login.component'; 5 | 6 | const routes: Routes = [ 7 | Shell.childRoutes([ 8 | { 9 | path: 'employee', 10 | loadChildren: () => import('./features/employee/employee.module').then((m) => m.EmployeeModule), 11 | //data: {role: 'Manager'}, 12 | }, 13 | { 14 | path: 'position', 15 | loadChildren: () => import('./features/position/position.module').then((m) => m.PositionModule), 16 | //data: {role: 'Manager'}, 17 | }, 18 | { 19 | path: 'admin', 20 | loadChildren: () => import('./features/admin/admin.module').then((m) => m.AdminModule), 21 | //data: {role: 'Manager'}, 22 | }, 23 | { 24 | path: 'manager', 25 | loadChildren: () => import('./features/manager/manager.module').then((m) => m.ManagerModule), 26 | }, 27 | { 28 | path: 'about', 29 | loadChildren: () => import('./about/about.module').then((m) => m.AboutModule), 30 | }, 31 | ]), 32 | 33 | { path: 'should-login', component: ShouldLoginComponent }, 34 | // Fallback when no prior route is matched 35 | { path: '**', redirectTo: '', pathMatch: 'full' }, 36 | ]; 37 | 38 | @NgModule({ 39 | imports: [ 40 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules, paramsInheritanceStrategy: 'always' }), 41 | ], 42 | exports: [RouterModule], 43 | providers: [], 44 | }) 45 | export class AppRoutingModule {} 46 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { CoreModule } from '@core'; 6 | import { AppComponent } from './app.component'; 7 | 8 | describe('AppComponent', () => { 9 | beforeEach( 10 | waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [RouterTestingModule, TranslateModule.forRoot(), CoreModule], 13 | declarations: [AppComponent], 14 | providers: [], 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | it( 20 | 'should create the app', 21 | waitForAsync(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }), 26 | 30000 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | import { merge } from 'rxjs'; 6 | import { filter, map, switchMap } from 'rxjs/operators'; 7 | 8 | import { environment } from '@env/environment'; 9 | import { Logger, UntilDestroy, untilDestroyed } from '@core'; 10 | import { I18nService } from '@app/i18n'; 11 | 12 | import { Observable } from 'rxjs'; 13 | import { AuthService } from '@app/@core/auth/auth.service'; 14 | 15 | const log = new Logger('App'); 16 | 17 | @UntilDestroy() 18 | @Component({ 19 | selector: 'app-root', 20 | templateUrl: './app.component.html', 21 | styleUrls: ['./app.component.scss'], 22 | }) 23 | export class AppComponent implements OnInit, OnDestroy { 24 | //isAuthenticated: Observable; 25 | 26 | constructor( 27 | private router: Router, 28 | private activatedRoute: ActivatedRoute, 29 | private titleService: Title, 30 | private translateService: TranslateService, 31 | private i18nService: I18nService, 32 | private authService: AuthService 33 | ) { 34 | //this.isAuthenticated = this.authService.isAuthenticated$; 35 | this.authService.runInitialLoginSequence(); 36 | } 37 | 38 | ngOnInit() { 39 | // Setup logger 40 | if (environment.production) { 41 | Logger.enableProductionMode(); 42 | } 43 | 44 | log.debug('init'); 45 | 46 | // Setup translations 47 | this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages); 48 | 49 | const onNavigationEnd = this.router.events.pipe(filter((event) => event instanceof NavigationEnd)); 50 | 51 | // Change page title on navigation or language change, based on route data 52 | merge(this.translateService.onLangChange, onNavigationEnd) 53 | .pipe( 54 | map(() => { 55 | let route = this.activatedRoute; 56 | while (route.firstChild) { 57 | route = route.firstChild; 58 | } 59 | return route; 60 | }), 61 | filter((route) => route.outlet === 'primary'), 62 | switchMap((route) => route.data), 63 | untilDestroyed(this) 64 | ) 65 | .subscribe((event) => { 66 | const title = event.title; 67 | if (title) { 68 | this.titleService.setTitle(this.translateService.instant(title)); 69 | } 70 | }); 71 | } 72 | 73 | ngOnDestroy() { 74 | this.i18nService.destroy(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | 9 | import { environment } from '@env/environment'; 10 | import { CoreModule } from '@core'; 11 | import { SharedModule } from '@shared'; 12 | import { HomeModule } from './home/home.module'; 13 | import { ShellModule } from './shell/shell.module'; 14 | import { AppComponent } from './app.component'; 15 | import { AppRoutingModule } from './app-routing.module'; 16 | import { FallbackComponent } from './fallback.component'; 17 | import { ShouldLoginComponent } from './should-login.component'; 18 | 19 | import { NgHttpLoaderModule } from 'ng-http-loader'; 20 | @NgModule({ 21 | imports: [ 22 | BrowserModule, 23 | ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }), 24 | FormsModule, 25 | HttpClientModule, 26 | TranslateModule.forRoot(), 27 | NgHttpLoaderModule.forRoot(), 28 | NgbModule, 29 | CoreModule.forRoot(), 30 | SharedModule, 31 | ShellModule, 32 | HomeModule, 33 | AppRoutingModule, // must be imported as the last module as it contains the fallback route 34 | ], 35 | declarations: [AppComponent, FallbackComponent, ShouldLoginComponent], 36 | providers: [], 37 | bootstrap: [AppComponent], 38 | }) 39 | export class AppModule {} 40 | -------------------------------------------------------------------------------- /src/app/config/constants.ts: -------------------------------------------------------------------------------- 1 | // Angular Modules 2 | import { Injectable } from '@angular/core'; 3 | import { environment } from '@env/environment'; 4 | 5 | @Injectable() 6 | export class Constants { 7 | public readonly Api_Endpoint: string = environment.Api_Endpoint; 8 | public readonly Api_Mock_Endpoint: string = environment.Api_Mock_Endpoint; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/fallback.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-fallback', 5 | template: `

This is the 🕳️ FALLBACK component.

`, 6 | }) 7 | export class FallbackComponent {} 8 | -------------------------------------------------------------------------------- /src/app/features/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AdminComponent } from './admin.component'; 4 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 5 | import { AuthGuard } from '@app/@core/auth/auth-guard.service'; 6 | import { RoleGuard } from '@app/@core/auth/role-guard.service'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: AdminComponent, 12 | canActivate: [AuthGuard, RoleGuard], 13 | data: { 14 | title: marker('Admin'), 15 | role: 'HrAdmin', 16 | }, 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class AdminRoutingModule {} 25 | -------------------------------------------------------------------------------- /src/app/features/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |

admin works!

2 | -------------------------------------------------------------------------------- /src/app/features/admin/admin.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/features/admin/admin.component.scss -------------------------------------------------------------------------------- /src/app/features/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminComponent } from './admin.component'; 4 | 5 | describe('AdminComponent', () => { 6 | let component: AdminComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [AdminComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(AdminComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin', 5 | templateUrl: './admin.component.html', 6 | styleUrls: ['./admin.component.scss'], 7 | }) 8 | export class AdminComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AdminRoutingModule } from './admin-routing.module'; 5 | import { AdminComponent } from './admin.component'; 6 | 7 | @NgModule({ 8 | declarations: [AdminComponent], 9 | imports: [CommonModule, AdminRoutingModule], 10 | }) 11 | export class AdminModule {} 12 | -------------------------------------------------------------------------------- /src/app/features/employee/detail/detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Mock Code - Detail Page

3 |

Source code: src\app\features\employee\detail\detail.component

4 |

This page is a placeholder for displaying record detail information.

5 |
6 | -------------------------------------------------------------------------------- /src/app/features/employee/detail/detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/features/employee/detail/detail.component.scss -------------------------------------------------------------------------------- /src/app/features/employee/detail/detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DetailComponent } from './detail.component'; 4 | 5 | describe('DetailComponent', () => { 6 | let component: DetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [DetailComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(DetailComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features/employee/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-detail', 5 | templateUrl: './detail.component.html', 6 | styleUrls: ['./detail.component.scss'], 7 | }) 8 | export class DetailComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/employee/employee-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { EmployeeComponent } from './employee.component'; 4 | import { DetailComponent } from './detail/detail.component'; 5 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 6 | import { AuthGuard } from '@app/@core/auth/auth-guard.service'; 7 | import { RoleGuard } from '@app/@core/auth/role-guard.service'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: EmployeeComponent, 13 | canActivate: [AuthGuard, RoleGuard], 14 | data: { title: marker('Employee'), role: 'Employee' }, 15 | }, 16 | { 17 | path: 'detail', 18 | component: DetailComponent, 19 | canActivate: [AuthGuard, RoleGuard], 20 | data: { title: marker('Employee Detail'), role: 'Employee' }, 21 | }, 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forChild(routes)], 26 | exports: [RouterModule], 27 | }) 28 | export class EmployeeRoutingModule {} 29 | -------------------------------------------------------------------------------- /src/app/features/employee/employee.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Mock Code - List Page

3 | 4 |

Source code: src\app\features\employee\employee.component

5 | 6 |

7 | This page is a placeholder for displaying a list of records with navigation to the 8 | detail page. 9 |

10 |
11 | -------------------------------------------------------------------------------- /src/app/features/employee/employee.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/features/employee/employee.component.scss -------------------------------------------------------------------------------- /src/app/features/employee/employee.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EmployeeComponent } from './employee.component'; 4 | 5 | describe('EmployeeComponent', () => { 6 | let component: EmployeeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [EmployeeComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(EmployeeComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features/employee/employee.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-employee', 5 | templateUrl: './employee.component.html', 6 | styleUrls: ['./employee.component.scss'], 7 | }) 8 | export class EmployeeComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/employee/employee.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { EmployeeRoutingModule } from './employee-routing.module'; 5 | import { EmployeeComponent } from './employee.component'; 6 | import { DetailComponent } from './detail/detail.component'; 7 | 8 | @NgModule({ 9 | declarations: [EmployeeComponent, DetailComponent], 10 | imports: [CommonModule, EmployeeRoutingModule], 11 | }) 12 | export class EmployeeModule {} 13 | -------------------------------------------------------------------------------- /src/app/features/manager/manager-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { ManagerComponent } from './manager.component'; 4 | import { AuthGuard } from '@app/@core/auth/auth-guard.service'; 5 | import { RoleGuard } from '@app/@core/auth/role-guard.service'; 6 | const routes: Routes = [ 7 | //{ path: '', component: ManagerComponent, canActivate: [AuthGuard] }, 8 | { path: '', component: ManagerComponent, canActivate: [AuthGuard, RoleGuard], data: { role: 'Manager' } }, 9 | //{ path: '', component: ManagerComponent, data: { role: 'Manager' } } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class ManagerRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/features/manager/manager.component.html: -------------------------------------------------------------------------------- 1 |

manager works!

2 | -------------------------------------------------------------------------------- /src/app/features/manager/manager.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/features/manager/manager.component.scss -------------------------------------------------------------------------------- /src/app/features/manager/manager.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ManagerComponent } from './manager.component'; 4 | 5 | describe('ManagerComponent', () => { 6 | let component: ManagerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ManagerComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ManagerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features/manager/manager.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-manager', 5 | templateUrl: './manager.component.html', 6 | styleUrls: ['./manager.component.scss'], 7 | }) 8 | export class ManagerComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/manager/manager.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ManagerRoutingModule } from './manager-routing.module'; 5 | import { ManagerComponent } from './manager.component'; 6 | 7 | @NgModule({ 8 | declarations: [ManagerComponent], 9 | imports: [CommonModule, ManagerRoutingModule], 10 | }) 11 | export class ManagerModule {} 12 | -------------------------------------------------------------------------------- /src/app/features/position/detail/detail.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

Position

7 |
8 | 9 | Back 10 |
11 |
12 |
13 |
14 | Position Number, Title, Description or Salary incorrect. 15 |
16 |
17 | 18 | 36 | 37 | 38 | 56 | 57 | 58 | 76 | 77 | 78 | 98 | 99 | 100 | 118 |
119 |
120 | 154 |
155 |
156 |
157 | -------------------------------------------------------------------------------- /src/app/features/position/detail/detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/features/position/detail/detail.component.scss -------------------------------------------------------------------------------- /src/app/features/position/detail/detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DetailComponent } from './detail.component'; 4 | 5 | describe('DetailComponent', () => { 6 | let component: DetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [DetailComponent], 13 | }).compileComponents(); 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(DetailComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/features/position/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 4 | import { Logger } from '@core'; 5 | import { ApiHttpService } from '@app/services/api-http.service'; 6 | import { ApiEndpointsService } from '@app/services/api-endpoints.service'; 7 | import { Position } from '@shared/models/position'; 8 | import { DataResponsePosition } from '@shared/classes/data-response-position'; 9 | import { ConfirmationDialogService } from '@app/services/confirmation-dialog.service'; 10 | import { RxwebValidators } from '@rxweb/reactive-form-validators'; 11 | import { ToastService } from '@app/services/toast.service'; 12 | 13 | const log = new Logger('Detail'); 14 | 15 | @Component({ 16 | selector: 'app-detail', 17 | templateUrl: './detail.component.html', 18 | styleUrls: ['./detail.component.scss'], 19 | }) 20 | export class DetailComponent implements OnInit { 21 | formMode = 'New'; 22 | sub: any; 23 | id: any; 24 | entryForm: FormGroup; 25 | error: string | undefined; 26 | position: Position; 27 | isAddNew: boolean = false; 28 | 29 | constructor( 30 | public toastService: ToastService, 31 | private route: ActivatedRoute, 32 | private formBuilder: FormBuilder, 33 | private apiHttpService: ApiHttpService, 34 | private apiEndpointsService: ApiEndpointsService, 35 | private confirmationDialogService: ConfirmationDialogService 36 | ) { 37 | this.createForm(); 38 | } 39 | 40 | ngOnInit() { 41 | this.sub = this.route.params.subscribe((params) => { 42 | this.id = params['id']; 43 | if (this.id !== undefined) { 44 | this.read(this.route.snapshot.paramMap.get('id')); 45 | this.formMode = 'Edit'; 46 | } else { 47 | this.isAddNew = true; 48 | this.formMode = 'New'; 49 | } 50 | }); 51 | log.debug('ngOnInit:', this.id); 52 | } 53 | 54 | // Handle Create button click 55 | onCreate() { 56 | this.create(this.entryForm.value); 57 | log.debug('OnInsert: ', this.entryForm.value); 58 | log.debug('OnInsert: ', this.entryForm.get('positionNumber').value); 59 | } 60 | 61 | // Handle Update button click 62 | onUpdate() { 63 | this.put(this.entryForm.get('id').value, this.entryForm.value); 64 | this.showSuccess('Great job!', 'Data is updated'); 65 | } 66 | 67 | // Handle Delete button click 68 | onDelete() { 69 | this.confirmationDialogService 70 | .confirm('Position deletion', 'Are you sure you want to delete?') 71 | .then((confirmed) => { 72 | if (confirmed) { 73 | this.delete(this.entryForm.get('id').value); 74 | log.debug('onDelete: ', this.entryForm.value); 75 | } 76 | }) 77 | .catch(() => { 78 | log.debug('onDelete: ', 'Cancel'); 79 | }); 80 | } 81 | // CRUD > Read, map to REST/HTTP GET 82 | read(id: any): void { 83 | this.apiHttpService.get(this.apiEndpointsService.getPositionByIdEndpoint(id), id).subscribe( 84 | //Assign resp to class-level model object. 85 | (resp: DataResponsePosition) => { 86 | //Assign data to class-level model object. 87 | this.position = resp.data; 88 | //Populate reactive form controls with model object properties. 89 | this.entryForm.setValue({ 90 | id: this.position.id, 91 | positionNumber: this.position.positionNumber, 92 | positionTitle: this.position.positionTitle, 93 | positionDescription: this.position.positionDescription, 94 | positionSalary: this.position.positionSalary, 95 | }); 96 | }, 97 | (error) => { 98 | log.debug(error); 99 | } 100 | ); 101 | } 102 | // CRUD > Delete, map to REST/HTTP DELETE 103 | delete(id: any): void { 104 | this.apiHttpService.delete(this.apiEndpointsService.deletePositionByIdEndpoint(id), id).subscribe( 105 | (resp: any) => { 106 | log.debug(resp); 107 | this.showSuccess('Great job!', 'Data is deleted'); 108 | this.entryForm.reset(); 109 | this.isAddNew = true; 110 | }, 111 | (error) => { 112 | log.debug(error); 113 | } 114 | ); 115 | } 116 | 117 | // CRUD > Create, map to REST/HTTP POST 118 | create(data: any): void { 119 | this.apiHttpService.post(this.apiEndpointsService.postPositionsEndpoint(), data).subscribe((resp: any) => { 120 | this.id = resp.data; //guid return in data 121 | this.showSuccess('Great job!', 'Data is inserted'); 122 | this.entryForm.reset(); 123 | }); 124 | } 125 | 126 | // CRUD > Update, map to REST/HTTP PUT 127 | put(id: string, data: any): void { 128 | this.apiHttpService.put(this.apiEndpointsService.putPositionsPagedEndpoint(id), data).subscribe((resp: any) => { 129 | this.id = resp.data; //guid return in data 130 | }); 131 | } 132 | 133 | // reactive form 134 | private createForm() { 135 | this.entryForm = this.formBuilder.group({ 136 | id: [''], 137 | positionNumber: ['', Validators.required], 138 | positionTitle: ['', Validators.required], 139 | positionDescription: ['', Validators.required], 140 | positionSalary: ['', RxwebValidators.numeric({ allowDecimal: true, isFormat: false })], 141 | }); 142 | } 143 | // ngbmodal service 144 | showSuccess(headerText: string, bodyText: string) { 145 | this.toastService.show(bodyText, { 146 | classname: 'bg-success text-light', 147 | delay: 2000, 148 | autohide: true, 149 | headertext: headerText, 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/app/features/position/position-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { PositionComponent } from './position.component'; 4 | import { DetailComponent } from './detail/detail.component'; 5 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 6 | import { AuthGuard } from '@app/@core/auth/auth-guard.service'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: PositionComponent, 12 | canActivate: [AuthGuard], 13 | data: { title: marker('Position') }, 14 | }, 15 | { 16 | path: 'detail', 17 | component: DetailComponent, 18 | canActivate: [AuthGuard], 19 | data: { title: marker('Position Detail') }, 20 | }, 21 | { 22 | path: 'detail/:id', 23 | component: DetailComponent, 24 | canActivate: [AuthGuard], 25 | data: { title: marker('Position Detail') }, 26 | }, 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forChild(routes)], 31 | exports: [RouterModule], 32 | }) 33 | export class PositionRoutingModule {} 34 | -------------------------------------------------------------------------------- /src/app/features/position/position.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Position

5 |
6 | 7 | Add 8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Position NumberPosition TitlePosition DescriptionPosition Salary
25 | 26 | {{ position.positionNumber }} 27 | {{ position.positionTitle }}{{ position.positionDescription }}{{ position.positionSalary }}
No data!
39 |
40 |
41 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /src/app/features/position/position.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep table.dataTable td.dataTables_empty { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/features/position/position.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PositionComponent } from './position.component'; 4 | 5 | describe('PositionComponent', () => { 6 | let component: PositionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [PositionComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(PositionComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features/position/position.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | 3 | import { Position } from '@shared/models/position'; 4 | import { ApiHttpService } from '@app/services/api-http.service'; 5 | import { ApiEndpointsService } from '@app/services/api-endpoints.service'; 6 | import { DataTablesResponse } from '@shared/classes/data-tables-response'; 7 | import { Logger } from '@core'; 8 | 9 | const log = new Logger('Position'); 10 | @Component({ 11 | selector: 'app-position', 12 | templateUrl: './position.component.html', 13 | styleUrls: ['./position.component.scss'], 14 | }) 15 | export class PositionComponent implements OnInit { 16 | dtOptions: DataTables.Settings = {}; 17 | positions: Position[]; 18 | message = ''; 19 | 20 | constructor(private apiHttpService: ApiHttpService, private apiEndpointsService: ApiEndpointsService) {} 21 | 22 | wholeRowClick(position: Position): void { 23 | log.debug('Whole row clicked.', position); 24 | } 25 | 26 | ngOnInit() { 27 | this.dtOptions = { 28 | pagingType: 'full_numbers', 29 | pageLength: 10, 30 | serverSide: true, 31 | processing: true, 32 | ajax: (dataTablesParameters: any, callback) => { 33 | //TODO Load from Akita 34 | //dataTablesParameters.start = 20; 35 | // Call WebAPI to get positions 36 | this.apiHttpService 37 | .post(this.apiEndpointsService.postPositionsPagedEndpoint(), dataTablesParameters) 38 | .subscribe((resp: DataTablesResponse) => { 39 | this.positions = resp.data; 40 | callback({ 41 | recordsTotal: resp.recordsTotal, 42 | recordsFiltered: resp.recordsFiltered, 43 | data: [], 44 | }); 45 | }); 46 | }, 47 | // Set column title and data field 48 | columns: [ 49 | { 50 | title: 'Number', 51 | data: 'positionNumber', 52 | }, 53 | { 54 | title: 'Title', 55 | data: 'positionTitle', 56 | }, 57 | { 58 | title: 'Description', 59 | data: 'positionDescription', 60 | }, 61 | { 62 | title: 'Salary', 63 | data: 'positionSalary', 64 | }, 65 | ], 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/features/position/position.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { SharedModule } from '@shared'; 5 | 6 | import { PositionRoutingModule } from './position-routing.module'; 7 | import { PositionComponent } from './position.component'; 8 | import { DetailComponent } from './detail/detail.component'; 9 | 10 | import { ReactiveFormsModule } from '@angular/forms'; 11 | import { RxReactiveFormsModule } from '@rxweb/reactive-form-validators'; 12 | 13 | import { DataTablesModule } from 'angular-datatables'; 14 | 15 | @NgModule({ 16 | declarations: [PositionComponent, DetailComponent], 17 | imports: [ 18 | CommonModule, 19 | TranslateModule, 20 | PositionRoutingModule, 21 | ReactiveFormsModule, 22 | SharedModule, 23 | RxReactiveFormsModule, 24 | DataTablesModule, 25 | ], 26 | }) 27 | export class PositionModule {} 28 | -------------------------------------------------------------------------------- /src/app/home/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/app/home/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/home/dashboard/dashboard.component.scss -------------------------------------------------------------------------------- /src/app/home/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [DashboardComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(DashboardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/home/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard', 5 | templateUrl: './dashboard.component.html', 6 | styleUrls: ['./dashboard.component.scss'], 7 | }) 8 | export class DashboardComponent implements OnInit { 9 | itemList: any; 10 | 11 | constructor() {} 12 | 13 | ngOnInit(): void { 14 | this.itemList = [ 15 | { 16 | position_count: 1001, 17 | employee_count: 40, 18 | assignment_count: 150, 19 | }, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 4 | 5 | import { HomeComponent } from './home.component'; 6 | import { Shell } from '@app/shell/shell.service'; 7 | 8 | const routes: Routes = [ 9 | Shell.childRoutes([ 10 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 11 | { path: 'home', component: HomeComponent, data: { title: marker('Home') } }, 12 | ]), 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule], 18 | providers: [], 19 | }) 20 | export class HomeRoutingModule {} 21 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 9 | 10 |
11 | Random slide 12 |
13 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 100px; 3 | } 4 | 5 | q { 6 | font-style: italic; 7 | font-size: 1.2rem; 8 | quotes: "« " " »"; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | 4 | import { CoreModule } from '@core'; 5 | import { SharedModule } from '@shared'; 6 | import { HomeComponent } from './home.component'; 7 | import { QuoteService } from './quote.service'; 8 | 9 | describe('HomeComponent', () => { 10 | let component: HomeComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [CoreModule, SharedModule, HttpClientTestingModule], 17 | declarations: [HomeComponent], 18 | providers: [QuoteService], 19 | }).compileComponents(); 20 | }) 21 | ); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(HomeComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgbCarouselConfig } from '@ng-bootstrap/ng-bootstrap'; 3 | import { Logger } from '@core'; 4 | 5 | const log = new Logger('app-home'); 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.scss'], 11 | providers: [NgbCarouselConfig], // add NgbCarouselConfig to the component providers 12 | }) 13 | export class HomeComponent implements OnInit { 14 | showNavigationArrows = true; 15 | showNavigationIndicators = true; 16 | images = [1077, 10, 1019, 1033, 1043].map((n) => `https://picsum.photos/id/${n}/1920/1080`); 17 | 18 | constructor() {} 19 | 20 | ngOnInit() {} 21 | } 22 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { SharedModule } from '@shared'; 6 | import { HomeRoutingModule } from './home-routing.module'; 7 | import { HomeComponent } from './home.component'; 8 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 9 | import { DashboardComponent } from './dashboard/dashboard.component'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule, TranslateModule, SharedModule, HomeRoutingModule, NgbModule], 13 | declarations: [HomeComponent, DashboardComponent], 14 | }) 15 | export class HomeModule {} 16 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 5 | 6 | import { LanguageSelectorComponent } from './language-selector.component'; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, TranslateModule, NgbModule], 10 | declarations: [LanguageSelectorComponent], 11 | exports: [LanguageSelectorComponent], 12 | }) 13 | export class I18nModule {} 14 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 3 | import { Subject } from 'rxjs'; 4 | 5 | import { I18nService } from './i18n.service'; 6 | 7 | const defaultLanguage = 'en-US'; 8 | const supportedLanguages = ['eo', 'en-US', 'fr-FR']; 9 | 10 | class MockTranslateService { 11 | currentLang = ''; 12 | onLangChange = new Subject(); 13 | 14 | use(language: string) { 15 | this.currentLang = language; 16 | this.onLangChange.next({ 17 | lang: this.currentLang, 18 | translations: {}, 19 | }); 20 | } 21 | 22 | getBrowserCultureLang() { 23 | return 'en-US'; 24 | } 25 | 26 | setTranslation(lang: string, translations: object, shouldMerge?: boolean) {} 27 | } 28 | 29 | describe('I18nService', () => { 30 | let i18nService: I18nService; 31 | let translateService: TranslateService; 32 | let onLangChangeSpy: jasmine.Spy; 33 | 34 | beforeEach(() => { 35 | TestBed.configureTestingModule({ 36 | providers: [I18nService, { provide: TranslateService, useClass: MockTranslateService }], 37 | }); 38 | 39 | i18nService = TestBed.inject(I18nService); 40 | translateService = TestBed.inject(TranslateService); 41 | 42 | // Create spies 43 | onLangChangeSpy = jasmine.createSpy('onLangChangeSpy'); 44 | translateService.onLangChange.subscribe((event: LangChangeEvent) => { 45 | onLangChangeSpy(event.lang); 46 | }); 47 | spyOn(translateService, 'use').and.callThrough(); 48 | }); 49 | 50 | afterEach(() => { 51 | // Cleanup 52 | localStorage.removeItem('language'); 53 | }); 54 | 55 | describe('init', () => { 56 | it('should init with default language', () => { 57 | // Act 58 | i18nService.init(defaultLanguage, supportedLanguages); 59 | 60 | // Assert 61 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 62 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 63 | }); 64 | 65 | it('should init with save language', () => { 66 | // Arrange 67 | const savedLanguage = 'eo'; 68 | localStorage.setItem('language', savedLanguage); 69 | 70 | // Act 71 | i18nService.init(defaultLanguage, supportedLanguages); 72 | 73 | // Assert 74 | expect(translateService.use).toHaveBeenCalledWith(savedLanguage); 75 | expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage); 76 | }); 77 | }); 78 | 79 | describe('set language', () => { 80 | it('should change current language', () => { 81 | // Arrange 82 | const newLanguage = 'eo'; 83 | i18nService.init(defaultLanguage, supportedLanguages); 84 | 85 | // Act 86 | i18nService.language = newLanguage; 87 | 88 | // Assert 89 | expect(translateService.use).toHaveBeenCalledWith(newLanguage); 90 | expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage); 91 | }); 92 | 93 | it('should change current language without a region match', () => { 94 | // Arrange 95 | const newLanguage = 'fr-CA'; 96 | i18nService.init(defaultLanguage, supportedLanguages); 97 | 98 | // Act 99 | i18nService.language = newLanguage; 100 | 101 | // Assert 102 | expect(translateService.use).toHaveBeenCalledWith('fr-FR'); 103 | expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR'); 104 | }); 105 | 106 | it('should change current language to default if unsupported', () => { 107 | // Arrange 108 | const newLanguage = 'es'; 109 | i18nService.init(defaultLanguage, supportedLanguages); 110 | 111 | // Act 112 | i18nService.language = newLanguage; 113 | 114 | // Assert 115 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 116 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 117 | }); 118 | }); 119 | 120 | describe('get language', () => { 121 | it('should return current language', () => { 122 | // Arrange 123 | i18nService.init(defaultLanguage, supportedLanguages); 124 | 125 | // Act 126 | const currentLanguage = i18nService.language; 127 | 128 | // Assert 129 | expect(currentLanguage).toEqual(defaultLanguage); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Logger } from '@core/logger.service'; 6 | import enUS from '../../translations/en-US.json'; 7 | 8 | const log = new Logger('I18nService'); 9 | const languageKey = 'language'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class I18nService { 15 | defaultLanguage!: string; 16 | supportedLanguages!: string[]; 17 | 18 | private langChangeSubscription!: Subscription; 19 | 20 | constructor(private translateService: TranslateService) { 21 | // Embed languages to avoid extra HTTP requests 22 | translateService.setTranslation('en-US', enUS); 23 | } 24 | 25 | /** 26 | * Initializes i18n for the application. 27 | * Loads language from local storage if present, or sets default language. 28 | * @param defaultLanguage The default language to use. 29 | * @param supportedLanguages The list of supported languages. 30 | */ 31 | init(defaultLanguage: string, supportedLanguages: string[]) { 32 | this.defaultLanguage = defaultLanguage; 33 | this.supportedLanguages = supportedLanguages; 34 | this.language = ''; 35 | 36 | // Warning: this subscription will always be alive for the app's lifetime 37 | this.langChangeSubscription = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { 38 | localStorage.setItem(languageKey, event.lang); 39 | }); 40 | } 41 | 42 | /** 43 | * Cleans up language change subscription. 44 | */ 45 | destroy() { 46 | if (this.langChangeSubscription) { 47 | this.langChangeSubscription.unsubscribe(); 48 | } 49 | } 50 | 51 | /** 52 | * Sets the current language. 53 | * Note: The current language is saved to the local storage. 54 | * If no parameter is specified, the language is loaded from local storage (if present). 55 | * @param language The IETF language code to set. 56 | */ 57 | set language(language: string) { 58 | language = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang(); 59 | let isSupportedLanguage = this.supportedLanguages.includes(language); 60 | 61 | // If no exact match is found, search without the region 62 | if (language && !isSupportedLanguage) { 63 | language = language.split('-')[0]; 64 | language = this.supportedLanguages.find((supportedLanguage) => supportedLanguage.startsWith(language)) || ''; 65 | isSupportedLanguage = Boolean(language); 66 | } 67 | 68 | // Fallback if language is not supported 69 | if (!isSupportedLanguage) { 70 | language = this.defaultLanguage; 71 | } 72 | 73 | log.debug(`Language set to ${language}`); 74 | this.translateService.use(language); 75 | } 76 | 77 | /** 78 | * Gets the current language. 79 | * @return The current language code. 80 | */ 81 | get language(): string { 82 | return this.translateService.currentLang; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.module'; 2 | export * from './i18n.service'; 3 | export * from './language-selector.component'; 4 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ currentLanguage | translate }} 4 | 5 | 6 | 9 | 10 |
11 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.scss: -------------------------------------------------------------------------------- 1 | .nav-link.dropdown-toggle { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | 4 | import { I18nService } from './i18n.service'; 5 | import { LanguageSelectorComponent } from './language-selector.component'; 6 | 7 | describe('LanguageSelectorComponent', () => { 8 | let component: LanguageSelectorComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach( 12 | waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [TranslateModule.forRoot()], 15 | declarations: [LanguageSelectorComponent], 16 | providers: [I18nService], 17 | }).compileComponents(); 18 | }) 19 | ); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(LanguageSelectorComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { I18nService } from './i18n.service'; 4 | 5 | @Component({ 6 | selector: 'app-language-selector', 7 | templateUrl: './language-selector.component.html', 8 | styleUrls: ['./language-selector.component.scss'], 9 | }) 10 | export class LanguageSelectorComponent implements OnInit { 11 | @Input() inNavbar = false; 12 | @Input() menuClass = ''; 13 | 14 | constructor(private i18nService: I18nService) {} 15 | 16 | ngOnInit() {} 17 | 18 | setLanguage(language: string) { 19 | this.i18nService.language = language; 20 | } 21 | 22 | get currentLanguage(): string { 23 | return this.i18nService.language; 24 | } 25 | 26 | get languages(): string[] { 27 | return this.i18nService.supportedLanguages; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/services/api-endpoints.service.ts: -------------------------------------------------------------------------------- 1 | // Angular Modules 2 | import { Injectable } from '@angular/core'; 3 | // Application Classes 4 | import { UrlBuilder } from '@shared/classes/url-builder'; 5 | import { QueryStringParameters } from '@shared/classes/query-string-parameters'; 6 | // Application Constants 7 | import { Constants } from '@app/config/constants'; 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | 12 | // Returns the api endpoints urls to use in services in a consistent way 13 | export class ApiEndpointsService { 14 | constructor( 15 | // Application Constants 16 | private constants: Constants 17 | ) {} 18 | 19 | /* #region EXAMPLES */ 20 | public getDataByIdEndpoint = (id: string): string => this.createUrlWithPathVariables('data', [id]); 21 | 22 | public getDataByIdAndCodeEndpoint = (id: string, code: number): string => 23 | this.createUrlWithPathVariables('data', [id, code]); 24 | 25 | public getDataByIdCodeAndYearEndpoint(id: string, code: number, year: number): string { 26 | const queryString: QueryStringParameters = new QueryStringParameters(); 27 | queryString.push('year', year); 28 | return `${this.createUrlWithPathVariables('data', [id, code])}?${queryString.toString()}`; 29 | } 30 | 31 | public getProductListByCountryCodeEndpoint(countryCode: string): string { 32 | return this.createUrlWithQueryParameters('productlist', (qs: QueryStringParameters) => 33 | qs.push('countryCode', countryCode) 34 | ); 35 | } 36 | 37 | public getProductListByCountryAndPostalCodeEndpoint(countryCode: string, postalCode: string): string { 38 | return this.createUrlWithQueryParameters('productlist', (qs: QueryStringParameters) => { 39 | qs.push('countryCode', countryCode); 40 | qs.push('postalCode', postalCode); 41 | }); 42 | } 43 | 44 | // call Mock endpoint 45 | public getNewsEndpoint = (): string => this.createUrl('41gRGwOaw', true); 46 | 47 | public invalidUrlEndpoint = (): string => this.createUrl('invalidurl', true); 48 | 49 | // call regular endpoint without boolean true at end 50 | public getPersonsEndpoint = (): string => this.createUrl('Persons'); 51 | 52 | // Call API technique https://medium.com/better-programming/angular-api-calls-the-right-way-264198bf2c64 53 | 54 | // call Mock endpoint 55 | // https://angular-datatables-demo-server.herokuapp.com 56 | public getPositionByIdEndpoint = (id: string): string => this.createUrlWithPathVariables('Positions', [id]); 57 | 58 | public deletePositionByIdEndpoint = (id: string): string => this.createUrlWithPathVariables('Positions', [id]); 59 | 60 | public postPersonsEndpoint = (): string => this.createUrl('', true); 61 | 62 | // call regular endpoint without boolean true at end 63 | // https://localhost:44378/api/v1 (ASP.NET CORE REST API. Repo https://github.com/workcontrolgit/AngularNgxDataTableBackend) 64 | public postPositionsPagedEndpoint = (): string => this.createUrl('Positions/Paged'); 65 | 66 | public postPositionsEndpoint = (): string => this.createUrl('Positions'); 67 | 68 | public putPositionsPagedEndpoint = (id: string): string => this.createUrlWithPathVariables('Positions', [id]); 69 | 70 | /* #endregion */ 71 | 72 | /* #region URL CREATOR */ 73 | // URL 74 | private createUrl(action: string, isMockAPI: boolean = false): string { 75 | const urlBuilder: UrlBuilder = new UrlBuilder( 76 | isMockAPI ? this.constants.Api_Mock_Endpoint : this.constants.Api_Endpoint, 77 | action 78 | ); 79 | return urlBuilder.toString(); 80 | } 81 | 82 | // URL WITH QUERY PARAMS 83 | private createUrlWithQueryParameters( 84 | action: string, 85 | queryStringHandler?: (queryStringParameters: QueryStringParameters) => void 86 | ): string { 87 | const urlBuilder: UrlBuilder = new UrlBuilder(this.constants.Api_Endpoint, action); 88 | // Push extra query string params 89 | if (queryStringHandler) { 90 | queryStringHandler(urlBuilder.queryString); 91 | } 92 | return urlBuilder.toString(); 93 | } 94 | 95 | // URL WITH PATH VARIABLES 96 | private createUrlWithPathVariables(action: string, pathVariables: any[] = []): string { 97 | let encodedPathVariablesUrl: string = ''; 98 | // Push extra path variables 99 | for (const pathVariable of pathVariables) { 100 | if (pathVariable !== null) { 101 | encodedPathVariablesUrl += `/${encodeURIComponent(pathVariable.toString())}`; 102 | } 103 | } 104 | const urlBuilder: UrlBuilder = new UrlBuilder(this.constants.Api_Endpoint, `${action}${encodedPathVariablesUrl}`); 105 | return urlBuilder.toString(); 106 | } 107 | /* #endregion */ 108 | } 109 | -------------------------------------------------------------------------------- /src/app/services/api-http.service.ts: -------------------------------------------------------------------------------- 1 | // Angular Modules 2 | import { Injectable } from '@angular/core'; 3 | import { HttpClient } from '@angular/common/http'; 4 | // RxJs 5 | import { Observable } from 'rxjs'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class ApiHttpService { 11 | constructor( 12 | // Angular Modules 13 | private http: HttpClient 14 | ) {} 15 | 16 | public get = (url: string, options?: any): Observable => this.http.get(url, options); 17 | 18 | public post = (url: string, data: any, options?: any): Observable => this.http.post(url, data, options); 19 | 20 | public put = (url: string, data: any, options?: any): Observable => this.http.put(url, data, options); 21 | 22 | public delete = (url: string, options?: any): Observable => this.http.delete(url, options); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/confirmation-dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgbModal, ModalDismissReasons, NgbModalOptions } from '@ng-bootstrap/ng-bootstrap'; 3 | import { ConfirmationDialogComponent } from '@shared/confirmation-dialog/confirmation-dialog.component'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ConfirmationDialogService { 9 | constructor(private modalService: NgbModal) {} 10 | 11 | public confirm( 12 | title: string, 13 | message: string, 14 | btnOkText: string = 'OK', 15 | btnCancelText: string = 'Cancel', 16 | dialogSize: 'sm' | 'lg' = 'sm' 17 | ): Promise { 18 | const modalRef = this.modalService.open(ConfirmationDialogComponent, { size: dialogSize }); 19 | modalRef.componentInstance.title = title; 20 | modalRef.componentInstance.message = message; 21 | modalRef.componentInstance.btnOkText = btnOkText; 22 | modalRef.componentInstance.btnCancelText = btnCancelText; 23 | return modalRef.result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/services/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, TemplateRef } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class ToastService { 7 | toasts: any[] = []; 8 | 9 | // Push new Toasts to array with content and options 10 | show(textOrTpl: string | TemplateRef, options: any = {}) { 11 | this.toasts.push({ textOrTpl, ...options }); 12 | } 13 | 14 | // Callback method to remove Toast DOM element from view 15 | remove(toast: any) { 16 | this.toasts = this.toasts.filter((t) => t !== toast); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 63 |
64 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | .navbar { 4 | margin-bottom: $spacer; 5 | } 6 | 7 | .nav-link.dropdown-toggle { 8 | cursor: pointer; 9 | } 10 | 11 | .logo { 12 | width: 35px; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | import { I18nModule } from '@app/i18n'; 7 | import { HeaderComponent } from './header.component'; 8 | 9 | describe('HeaderComponent', () => { 10 | let component: HeaderComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [RouterTestingModule, NgbModule, TranslateModule.forRoot(), I18nModule], 17 | declarations: [HeaderComponent], 18 | providers: [], 19 | }).compileComponents(); 20 | }) 21 | ); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(HeaderComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { AuthService } from '@app/@core/auth/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'], 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | menuHidden = true; 13 | isAuthenticated: Observable; 14 | 15 | constructor(private authService: AuthService) { 16 | this.isAuthenticated = authService.isAuthenticated$; 17 | } 18 | 19 | ngOnInit() {} 20 | 21 | toggleMenu() { 22 | this.menuHidden = !this.menuHidden; 23 | } 24 | 25 | login() { 26 | this.authService.login(); 27 | } 28 | 29 | logout() { 30 | //this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); 31 | this.authService.logout(); 32 | } 33 | 34 | // get username(): string | null { 35 | // this.profileData$ = this.authService.; 36 | // this.profileData$.subscribe((data) => { 37 | // this.name = data.name; 38 | // //console.log (this.name) 39 | // }); 40 | // return this.name ? this.name : null; 41 | // } 42 | 43 | get username(): string | null { 44 | return this.authService.identityClaims ? (this.authService.identityClaims as any)['email'] : null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/app/shell/shell.component.scss -------------------------------------------------------------------------------- /src/app/shell/shell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 5 | 6 | import { CoreModule } from '@core'; 7 | 8 | import { I18nModule } from '@app/i18n'; 9 | import { ShellComponent } from './shell.component'; 10 | import { HeaderComponent } from './header/header.component'; 11 | 12 | describe('ShellComponent', () => { 13 | let component: ShellComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach( 17 | waitForAsync(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [RouterTestingModule, TranslateModule.forRoot(), I18nModule, NgbModule, CoreModule], 20 | declarations: [HeaderComponent, ShellComponent], 21 | }).compileComponents(); 22 | }) 23 | ); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(ShellComponent); 27 | component = fixture.componentInstance; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('should create', () => { 32 | expect(component).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-shell', 5 | templateUrl: './shell.component.html', 6 | styleUrls: ['./shell.component.scss'], 7 | }) 8 | export class ShellComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shell/shell.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 6 | 7 | import { I18nModule } from '@app/i18n'; 8 | import { ShellComponent } from './shell.component'; 9 | import { HeaderComponent } from './header/header.component'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule, TranslateModule, NgbModule, I18nModule, RouterModule], 13 | declarations: [HeaderComponent, ShellComponent], 14 | }) 15 | export class ShellModule {} 16 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ShellComponent } from './shell.component'; 4 | import { Shell } from './shell.service'; 5 | 6 | describe('Shell', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ShellComponent], 10 | }); 11 | }); 12 | 13 | describe('childRoutes', () => { 14 | it('should create routes as children of shell', () => { 15 | // Prepare 16 | const testRoutes = [{ path: 'test' }]; 17 | 18 | // Act 19 | const result = Shell.childRoutes(testRoutes); 20 | 21 | // Assert 22 | expect(result.path).toBe(''); 23 | expect(result.children).toBe(testRoutes); 24 | expect(result.component).toBe(ShellComponent); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Route, ActivatedRoute } from '@angular/router'; 2 | import { AuthGuard } from '@app/@core/auth/auth-guard.service'; 3 | import { AuthGuardWithForcedLogin } from '@app/@core/auth/auth-guard-with-forced-login.service'; 4 | import { ShellComponent } from './shell.component'; 5 | 6 | /** 7 | * Provides helper methods to create routes. 8 | */ 9 | export class Shell { 10 | // pageTitle: string; 11 | // constructor( 12 | // private route: ActivatedRoute 13 | // ) {} 14 | /** 15 | * Creates routes using the shell component and authentication. 16 | * @param routes The routes to add. 17 | * @return The new route using shell as the base. 18 | */ 19 | static childRoutes(routes: Routes): Route { 20 | return { 21 | path: '', 22 | component: ShellComponent, 23 | children: routes, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/should-login.component.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/app/should-login.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | @import "~bootstrap/scss/mixins/_breakpoints"; 3 | 4 | .login-container { 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | } 11 | 12 | .login-box { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | width: 100%; 18 | min-height: 100%; 19 | } 20 | 21 | .ng-invalid.ng-touched:not(form) { 22 | border-left: 4px solid theme-color("danger"); 23 | } 24 | 25 | @include media-breakpoint-down(xs) { 26 | .container { 27 | width: 100%; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/should-login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { OAuthService } from 'angular-oauth2-oidc'; 3 | import { AuthService } from '@app/@core/auth/auth.service'; 4 | import { environment } from '@env/environment'; 5 | 6 | @Component({ 7 | selector: 'app-should-login', 8 | templateUrl: './should-login.component.html', 9 | styleUrls: ['./should-login.component.scss'], 10 | }) 11 | export class ShouldLoginComponent { 12 | version: string | null = environment.version; 13 | constructor(private authService: AuthService, private oAuthService: OAuthService) {} 14 | 15 | public login($event: any) { 16 | $event.preventDefault(); 17 | this.oAuthService.initLoginFlow(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons8-angularjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/ngx-rocket-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/assets/ngx-rocket-logo.png -------------------------------------------------------------------------------- /src/assets/ngx-rocket-logo@192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/assets/ngx-rocket-logo@192.png -------------------------------------------------------------------------------- /src/environments/.env.ts: -------------------------------------------------------------------------------- 1 | export const env: { [s: string]: string | null } = { 2 | npm_package_version: '1.0.0', 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // `.env.ts` is generated by the `npm run env` command 2 | // `npm run env` exposes environment variables as JSON for any usage you might 3 | // want, like displaying the version or getting extra config from your CI bot, etc. 4 | // This is useful for granularity you might need beyond just the environment. 5 | // Note that as usual, any environment variables you expose through it will end up in your 6 | // bundle, and you should not use it for any sensitive information like passwords or keys. 7 | import { env } from './.env'; 8 | 9 | export const environment = { 10 | production: true, 11 | version: env.npm_package_version, 12 | serverUrl: 'https://api.chucknorris.io', 13 | defaultLanguage: 'en-US', 14 | supportedLanguages: ['en-US'], 15 | // REST API server 16 | Api_Endpoint: 'https://cat-netcore-api.azurewebsites.net/api/v1', 17 | Api_Mock_Endpoint: 'https://angular-datatables-demo-server.herokuapp.com', 18 | 19 | // IdentityServer/OIDC Configuration 20 | // Oidc_Issuer: 'https://localhost:44310', //this is for IdentityServer4 Admin UI running on localhost 21 | Oidc_Issuer: 'https://cat-token-identity.azurewebsites.net', //demo identityserver4 in Azure 22 | Oidc_ClientId: 'AngularStarterKit', // client id setup in IdentityServer4 23 | Oidc_responseType: 'code', //code flow PKCE, 24 | Oidc_redirectUri: window.location.origin + '/auth-callback', 25 | Oidc_postLogoutRedirectUri: window.location.origin, 26 | Oidc_silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 27 | Oidc_scope: 'openid profile email roles app.api.employeeprofile.read', // Ask offline_access to support refresh token refreshes 28 | Oidc_useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes 29 | Oidc_silentRefreshTimeout: 5000, // For faster testing 30 | Oidc_timeoutFactor: 0.25, // For faster testing 31 | Oidc_sessionChecksEnabled: true, 32 | Oidc_showDebugInformation: true, // Also requires enabling "Verbose" level in devtools 33 | Oidc_clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, 34 | Oidc_nonceStateSeparator: 'semicolon', // Real semicolon gets mangled by IdentityServer's URI encoding 35 | }; 36 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | // `.env.ts` is generated by the `npm run env` command 6 | // `npm run env` exposes environment variables as JSON for any usage you might 7 | // want, like displaying the version or getting extra config from your CI bot, etc. 8 | // This is useful for granularity you might need beyond just the environment. 9 | // Note that as usual, any environment variables you expose through it will end up in your 10 | // bundle, and you should not use it for any sensitive information like passwords or keys. 11 | import { env } from './.env'; 12 | 13 | export const environment = { 14 | production: false, 15 | version: env.npm_package_version + '-dev', 16 | serverUrl: '/api', 17 | defaultLanguage: 'en-US', 18 | supportedLanguages: ['en-US'], 19 | //REST API server 20 | Api_Endpoint: 'https://localhost:44378/api/v1', 21 | //Api_Endpoint: 'https://cat-netcore-api.azurewebsites.net/api/v1', 22 | Api_Mock_Endpoint: 'https://angular-datatables-demo-server.herokuapp.com', 23 | 24 | //IdentityServer/OIDC Configuration 25 | Oidc_Issuer: 'https://localhost:44310', //this is for IdentityServer4 Admin UI running on localhost https://github.com/workcontrolgit/TokenProject.AdminUI 26 | //Oidc_Issuer: 'https://cat-token-identity.azurewebsites.net', //demo identityserver4 in Azure 27 | Oidc_ClientId: 'AngularStarterKit', // client id setup in IdentityServer4 28 | Oidc_responseType: 'code', //code flow PKCE, https://github.com/workcontrolgit/TokenProject.AdminUI 29 | Oidc_redirectUri: window.location.origin + '/auth-callback', 30 | Oidc_postLogoutRedirectUri: window.location.origin, 31 | Oidc_silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 32 | Oidc_scope: 'openid profile email roles app.api.employeeprofile.read', // Ask offline_access to support refresh token refreshes 33 | Oidc_useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes 34 | Oidc_silentRefreshTimeout: 50000, // For faster testing 35 | Oidc_timeoutFactor: 0.25, // For faster testing 36 | Oidc_sessionChecksEnabled: true, 37 | Oidc_showDebugInformation: true, // Also requires enabling "Verbose" level in devtools 38 | Oidc_clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, 39 | Oidc_nonceStateSeparator: 'semicolon', // Real semicolon gets mangled by IdentityServer's URI encoding 40 | }; 41 | 42 | /* 43 | * For easier debugging in development mode, you can import the following file 44 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 45 | * 46 | * This import should be commented out in production mode because it will have a negative impact 47 | * on performance if an error is thrown. 48 | */ 49 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 50 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CATToolkitAngular 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of global application style. 3 | * Component-specific style should not go here and be included directly as part of the components. 4 | */ 5 | 6 | // Theme variables, must be included before the libraries to allow overriding defaults 7 | @import "theme/theme-variables"; 8 | 9 | // 3rd party libraries 10 | @import "~bootstrap/scss/bootstrap"; 11 | @import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; 12 | @import "~@fortawesome/fontawesome-free/scss/brands.scss"; 13 | @import "~@fortawesome/fontawesome-free/scss/regular.scss"; 14 | @import "~@fortawesome/fontawesome-free/scss/solid.scss"; 15 | 16 | // Theme customization 17 | @import "theme/theme"; 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of the application. 3 | * Only platform bootstrapping code should be here. 4 | * For app-specific initialization, use `app/app.component.ts`. 5 | */ 6 | 7 | import { enableProdMode } from '@angular/core'; 8 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 9 | 10 | import { AppModule } from '@app/app.module'; 11 | import { environment } from '@env/environment'; 12 | 13 | if (environment.production) { 14 | enableProdMode(); 15 | } 16 | 17 | platformBrowserDynamic() 18 | .bootstrapModule(AppModule) 19 | .catch((err) => console.error(err)); 20 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CATToolkitAngular", 3 | "short_name": "CATToolkitAngular", 4 | "theme_color": "#488aff", 5 | "background_color": "#488aff", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/ngx-rocket-logo.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/ngx-rocket-logo@192.png", 18 | "sizes": "192x192", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | 6 | /** 7 | * This file includes polyfills needed by Angular and is loaded before the app. 8 | * You can add your own extra polyfills to this file. 9 | * 10 | * This file is divided into 2 sections: 11 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 12 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 13 | * file. 14 | * 15 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 16 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 17 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 18 | * 19 | * Learn more in https://angular.io/guide/browser-support 20 | */ 21 | 22 | /*************************************************************************************************** 23 | * BROWSER POLYFILLS 24 | */ 25 | 26 | /** 27 | * By default, zone.js will patch all possible macroTask and DomEvents 28 | * user can disable parts of macroTask/DomEvents patch by setting following flags 29 | * because those flags need to be set before `zone.js` being loaded, and webpack 30 | * will put import in the top of bundle, so user need to create a separate file 31 | * in this directory (for example: zone-flags.ts), and put the following flags 32 | * into that file, and then add the following code before importing zone.js. 33 | * import './zone-flags'; 34 | * 35 | * The flags allowed in zone-flags.ts are listed here. 36 | * 37 | * The following flags will work for all browsers. 38 | * 39 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 40 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 41 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 42 | * 43 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 44 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 45 | * 46 | * (window as any).__Zone_enable_cross_context_check = true; 47 | * 48 | */ 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by default for Angular itself. 52 | */ 53 | import 'zone.js'; // Included with Angular CLI. 54 | 55 | /*************************************************************************************************** 56 | * APPLICATION IMPORTS 57 | */ 58 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/silent-refresh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Prepare environment for unit tests. 3 | * This file is required by karma.conf.js and loads recursively all the .spec and framework files. 4 | */ 5 | 6 | import 'zone.js/testing'; 7 | import { getTestBed } from '@angular/core/testing'; 8 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context( 12 | path: string, 13 | deep?: boolean, 14 | filter?: RegExp 15 | ): { 16 | keys(): string[]; 17 | (id: string): T; 18 | }; 19 | }; 20 | 21 | // First, initialize the Angular testing environment. 22 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 23 | teardown: { destroyAfterEach: false }, 24 | }); 25 | // Then we find all the tests. 26 | const context = require.context('./', true, /\.spec\.ts$/); 27 | // And load the modules. 28 | context.keys().map(context); 29 | -------------------------------------------------------------------------------- /src/theme/theme-variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Application global variables. 3 | */ 4 | 5 | // Set Font Awesome font path 6 | $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; 7 | 8 | // --------------------------------------------------------------------------- 9 | // Bootstrap variables 10 | // 11 | // Override Bootstrap variables here to suite your theme. 12 | // Copy variables you want to customize from node_modules/bootstrap/scss/_variables.scss 13 | 14 | // 15 | // Color system 16 | // 17 | 18 | $white: #fff; 19 | $gray-100: #f8f9fa; 20 | $gray-200: #e9ecef; 21 | $gray-300: #dee2e6; 22 | $gray-400: #ced4da; 23 | $gray-500: #adb5bd; 24 | $gray-600: #868e96; 25 | $gray-700: #495057; 26 | $gray-800: #343a40; 27 | $gray-900: #212529; 28 | $black: #000; 29 | 30 | $blue: #0073dd; 31 | $indigo: #6610f2; 32 | $purple: #6f42c1; 33 | $pink: #e83e8c; 34 | $red: #dc3545; 35 | $orange: #fd7e14; 36 | $yellow: #ffc107; 37 | $green: #28a745; 38 | $teal: #20c997; 39 | $cyan: #17a2b8; 40 | 41 | $theme-colors: ( 42 | primary: $blue, 43 | secondary: $gray-600, 44 | success: $green, 45 | info: $cyan, 46 | warning: $yellow, 47 | danger: $red, 48 | light: $gray-100, 49 | dark: $gray-800, 50 | ); 51 | 52 | // Use Bootstrap defaults for other variables, imported here so we can access all app variables in one place when used 53 | // in components. 54 | @import "~bootstrap/scss/_functions"; 55 | @import "~bootstrap/scss/_variables"; 56 | -------------------------------------------------------------------------------- /src/theme/theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Global application theme. 3 | * Framework overrides and customization goes here. 4 | */ 5 | -------------------------------------------------------------------------------- /src/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "Starter Kit", 3 | "Detail": "Detail", 4 | "Hello world !": "Hello world !", 5 | "Master": "Master", 6 | "Version": "Version" 7 | } 8 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Extra typings definitions 3 | */ 4 | 5 | // Allow .json files imports 6 | declare module '*.json'; 7 | 8 | // SystemJS module definition 9 | declare var module: NodeModule; 10 | interface NodeModule { 11 | id: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /stats.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workcontrolgit/cat-toolkit-angular-starter/2039f8efae63ece4c6d48c14f81bd8864c5c61d9/stats.json -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "importHelpers": true, 12 | "noImplicitAny": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "target": "es2015", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["es2018", "dom"], 17 | "baseUrl": "./", 18 | "paths": { 19 | "@app/*": ["src/app/*"], 20 | "@core": ["src/app/@core"], 21 | "@core/*": ["src/app/@core/*"], 22 | "@shared": ["src/app/@shared"], 23 | "@shared/*": ["src/app/@shared/*"], 24 | "@env/*": ["src/environments/*"] 25 | } 26 | }, 27 | "angularCompilerOptions": { 28 | "fullTemplateTypeCheck": true, 29 | "strictInjectionParameters": true, 30 | "preserveWhitespaces": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.mock.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rulesDirectory": ["codelyzer"], 4 | "rules": { 5 | "array-type": false, 6 | "arrow-parens": false, 7 | "deprecation": { 8 | "severity": "warning" 9 | }, 10 | "import-blacklist": [true, "rxjs/Rx"], 11 | "max-line-length": [true, 120], 12 | "member-access": false, 13 | "member-ordering": [ 14 | true, 15 | { 16 | "order": [ 17 | "public-static-field", 18 | "protected-static-field", 19 | "private-static-field", 20 | "public-instance-field", 21 | "protected-instance-field", 22 | "private-instance-field", 23 | "public-static-method", 24 | "protected-static-method", 25 | "private-static-method", 26 | "constructor", 27 | "public-instance-method", 28 | "protected-instance-method", 29 | "private-instance-method" 30 | ] 31 | } 32 | ], 33 | "interface-name": false, 34 | "max-classes-per-file": false, 35 | "no-consecutive-blank-lines": false, 36 | "no-console": [true, "debug", "time", "timeEnd", "trace"], 37 | "no-duplicate-variable": [true, "check-parameters"], 38 | "no-empty": false, 39 | "no-inferrable-types": [true, "ignore-params"], 40 | "no-non-null-assertion": true, 41 | "no-redundant-jsdoc": true, 42 | "no-switch-case-fall-through": true, 43 | "no-unnecessary-initializer": true, 44 | "object-literal-sort-keys": false, 45 | "quotemark": [true, "single"], 46 | "typedef": [true, "parameter", "property-declaration"], 47 | "variable-name": false, 48 | "no-var-requires": false, 49 | "object-literal-key-quotes": false, 50 | "ordered-imports": false, 51 | "trailing-comma": false, 52 | "no-conflicting-lifecycle": true, 53 | "no-output-native": true, 54 | "directive-selector": [true, "attribute", "app", "camelCase"], 55 | "component-selector": [true, "element", "page", "app", "kebab-case"], 56 | "template-banana-in-box": true, 57 | "template-no-negated-async": true, 58 | "no-output-on-prefix": true, 59 | "no-inputs-metadata-property": true, 60 | "no-outputs-metadata-property": true, 61 | "no-host-metadata-property": true, 62 | "no-input-rename": true, 63 | "no-output-rename": true, 64 | "use-lifecycle-interface": true, 65 | "use-pipe-transform-interface": true, 66 | "component-class-suffix": true, 67 | "contextual-lifecycle": true, 68 | "directive-class-suffix": true 69 | } 70 | } 71 | --------------------------------------------------------------------------------