├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── app-injector.service.spec.ts │ ├── app-injector.service.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.config.ts │ ├── app.module.ts │ ├── demo-common │ │ ├── demo-common.module.ts │ │ ├── directives │ │ │ ├── component-header │ │ │ │ ├── component-header.component.html │ │ │ │ ├── component-header.component.scss │ │ │ │ ├── component-header.component.spec.ts │ │ │ │ └── component-header.component.ts │ │ │ ├── header-bar │ │ │ │ ├── header-bar.component.html │ │ │ │ ├── header-bar.component.scss │ │ │ │ ├── header-bar.component.spec.ts │ │ │ │ └── header-bar.component.ts │ │ │ ├── top-nav │ │ │ │ ├── top-nav.component.html │ │ │ │ ├── top-nav.component.scss │ │ │ │ ├── top-nav.component.spec.ts │ │ │ │ └── top-nav.component.ts │ │ │ └── transaction-confirm-dialog │ │ │ │ ├── transaction-confirm-dialog.component.css │ │ │ │ ├── transaction-confirm-dialog.component.html │ │ │ │ ├── transaction-confirm-dialog.component.spec.ts │ │ │ │ └── transaction-confirm-dialog.component.ts │ │ ├── models │ │ │ ├── account-search-result.models.ts │ │ │ ├── app-config.model.ts │ │ │ └── transaction.models.ts │ │ ├── services │ │ │ ├── demo-common-data.service.ts │ │ │ ├── demo-resolver.service.ts │ │ │ ├── search-data.service.ts │ │ │ ├── search.service.spec.ts │ │ │ ├── search.service.ts │ │ │ ├── transaction-data.service.ts │ │ │ ├── transaction-entity-data.service.ts │ │ │ ├── transaction.service.spec.ts │ │ │ ├── transaction.service.ts │ │ │ ├── user-session.service.spec.ts │ │ │ └── user-session.service.ts │ │ └── testing │ │ │ └── testing-helpers.ts │ ├── demo │ │ ├── accounts │ │ │ ├── child-component1 │ │ │ │ ├── child-component1-resolver.service.ts │ │ │ │ ├── child-component1.component.html │ │ │ │ ├── child-component1.component.scss │ │ │ │ ├── child-component1.component.spec.ts │ │ │ │ └── child-component1.component.ts │ │ │ ├── child-component2 │ │ │ │ ├── child-component2-resolver.service.ts │ │ │ │ ├── child-component2.component.html │ │ │ │ ├── child-component2.component.scss │ │ │ │ ├── child-component2.component.spec.ts │ │ │ │ └── child-component2.component.ts │ │ │ ├── child-component3 │ │ │ │ ├── child-component3-resolver.service.ts │ │ │ │ ├── child-component3.component.html │ │ │ │ ├── child-component3.component.scss │ │ │ │ ├── child-component3.component.spec.ts │ │ │ │ └── child-component3.component.ts │ │ │ └── shared │ │ │ │ ├── components │ │ │ │ ├── demo-transaction-component.spec.ts │ │ │ │ ├── demo-transaction-component.ts │ │ │ │ └── navigation-error │ │ │ │ │ ├── navigation-error.component.html │ │ │ │ │ ├── navigation-error.component.scss │ │ │ │ │ ├── navigation-error.component.spec.ts │ │ │ │ │ └── navigation-error.component.ts │ │ │ │ ├── directives │ │ │ │ ├── account-header │ │ │ │ │ ├── account-header.component.html │ │ │ │ │ ├── account-header.component.scss │ │ │ │ │ ├── account-header.component.spec.ts │ │ │ │ │ ├── account-header.component.ts │ │ │ │ │ ├── account-header.service.spec.ts │ │ │ │ │ └── account-header.service.ts │ │ │ │ └── header-menu │ │ │ │ │ ├── header-menu.component.html │ │ │ │ │ ├── header-menu.component.scss │ │ │ │ │ └── header-menu.component.ts │ │ │ │ ├── models │ │ │ │ ├── account.models.ts │ │ │ │ ├── child1-entity.models.ts │ │ │ │ ├── child2-entity.models.ts │ │ │ │ └── child3-entity.models.ts │ │ │ │ └── services │ │ │ │ ├── account-data.service.ts │ │ │ │ ├── child1-data.service.ts │ │ │ │ ├── child2-data.service.ts │ │ │ │ ├── child3-data.service.ts │ │ │ │ ├── demo-account-resolver.service.ts │ │ │ │ ├── menu.service.ts │ │ │ │ └── update.service.ts │ │ ├── demo-routing.module.ts │ │ ├── demo.module.ts │ │ └── search │ │ │ └── search-results │ │ │ ├── search-results-resolver.service.ts │ │ │ ├── search-results.component.html │ │ │ ├── search-results.component.scss │ │ │ ├── search-results.component.spec.ts │ │ │ └── search-results.component.ts │ ├── framework │ │ ├── components │ │ │ ├── base-component.spec.ts │ │ │ └── base-component.ts │ │ ├── errorhandling │ │ │ ├── error-handler.service.spec.ts │ │ │ ├── error-handler.service.ts │ │ │ ├── error-utilities.service.spec.ts │ │ │ └── error-utilities.service.ts │ │ ├── framework.module.ts │ │ ├── logging │ │ │ ├── logging.service.ts │ │ │ └── severity-level.model.ts │ │ ├── models │ │ │ ├── authorization.types.ts │ │ │ ├── form-controls.models.ts │ │ │ └── metadata.models.ts │ │ ├── services │ │ │ ├── auth-guard.service.ts │ │ │ ├── auth-interceptor.service.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── authorization-data.service.ts │ │ │ ├── authorization.service.spec.ts │ │ │ ├── authorization.service.ts │ │ │ ├── can-deactivate-guard.service.ts │ │ │ ├── data.service.ts │ │ │ ├── form-builder.service.spec.ts │ │ │ ├── form-builder.service.ts │ │ │ ├── global-events.service.ts │ │ │ ├── metadata-data.service.ts │ │ │ ├── metadata.service.ts │ │ │ ├── system-message-data.service.ts │ │ │ ├── system-message.service.spec.ts │ │ │ ├── system-message.service.ts │ │ │ ├── url-serializer.service.ts │ │ │ ├── utilities.service.spec.ts │ │ │ ├── utilities.service.ts │ │ │ └── web-storage.service.ts │ │ └── validation │ │ │ ├── directives │ │ │ ├── control-validation.directives.spec.ts │ │ │ └── control-validators.directive.ts │ │ │ ├── models │ │ │ ├── server-error.models.ts │ │ │ └── validation.models.ts │ │ │ ├── services │ │ │ ├── validation.service.spec.ts │ │ │ └── validation.service.ts │ │ │ └── validation.module.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ ├── log-out │ │ │ ├── log-out.component.html │ │ │ ├── log-out.component.scss │ │ │ ├── log-out.component.spec.ts │ │ │ └── log-out.component.ts │ │ ├── page-error │ │ │ ├── page-error.component.html │ │ │ ├── page-error.component.scss │ │ │ ├── page-error.component.spec.ts │ │ │ └── page-error.component.ts │ │ └── page-not-found │ │ │ ├── page-not-found.component.html │ │ │ ├── page-not-found.component.scss │ │ │ ├── page-not-found.component.spec.ts │ │ │ └── page-not-found.component.ts │ └── shared │ │ ├── directives │ │ ├── accordion │ │ │ ├── accordion.component.css │ │ │ ├── accordion.component.html │ │ │ └── accordion.component.ts │ │ ├── alert │ │ │ ├── alert.component.css │ │ │ ├── alert.component.html │ │ │ └── alert.component.ts │ │ ├── calendar │ │ │ ├── calendar.component.css │ │ │ ├── calendar.component.html │ │ │ ├── calendar.component.spec.ts │ │ │ └── calendar.component.ts │ │ ├── confirm-dialog │ │ │ ├── confirm-dialog.component.css │ │ │ ├── confirm-dialog.component.html │ │ │ ├── confirm-dialog.component.ts │ │ │ └── parts-confirm-dialog.component.spec.ts │ │ ├── data-table │ │ │ ├── data-table-models.ts │ │ │ ├── data-table.component.html │ │ │ ├── data-table.component.scss │ │ │ ├── data-table.component.spec.ts │ │ │ └── data-table.component.ts │ │ ├── dialog │ │ │ ├── dialog.component.css │ │ │ ├── dialog.component.html │ │ │ └── dialog.component.ts │ │ ├── disable-if-unauthorized │ │ │ └── disable-if-unauthorized.directive.ts │ │ ├── error-message │ │ │ ├── error-message.component.html │ │ │ ├── error-message.component.scss │ │ │ ├── error-message.component.spec.ts │ │ │ └── error-message.component.ts │ │ ├── form-field │ │ │ ├── form-field.component.html │ │ │ ├── form-field.component.scss │ │ │ ├── form-field.component.spec.ts │ │ │ └── form-field.component.ts │ │ ├── form-list │ │ │ ├── form-list.component.html │ │ │ ├── form-list.component.scss │ │ │ └── form-list.component.ts │ │ ├── hide-if-unauthorized │ │ │ └── hide-if-unauthorized.directive.ts │ │ ├── input │ │ │ ├── input.component.html │ │ │ ├── input.component.scss │ │ │ ├── input.component.spec.ts │ │ │ └── input.component.ts │ │ └── menu │ │ │ ├── menu.component.html │ │ │ ├── menu.component.scss │ │ │ ├── menu.component.spec.ts │ │ │ └── menu.component.ts │ │ ├── models │ │ ├── app-monitor.ts │ │ └── confirm-choices.enum.ts │ │ ├── pipes │ │ ├── date-to-string.pipe.spec.ts │ │ ├── date-to-string.pipe.ts │ │ ├── display-error.pipe.spec.ts │ │ └── display-error.pipe.ts │ │ └── shared.module.ts ├── assets │ ├── config │ │ ├── config.deploy.json │ │ ├── config.dev.json │ │ ├── config.local.json │ │ └── config.prod.json │ └── loading.gif ├── environments │ ├── environment.deploy.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── scss │ ├── colors.scss │ ├── dimensions.scss │ └── mixins.scss ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── typings.d.ts ├── variables.less └── web.config ├── tsconfig.json ├── tslint.json └── website.publishproj /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | TESTS-Chrome*.xml 46 | TESTS-HeadlessChrome*.xml 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgPatternsDemo 2 | 3 | ## Development server 4 | Run npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 5 | 6 | ## Build 7 | 8 | Run `npm run prod` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 9 | 10 | ## Running unit tests 11 | 12 | Run `npm run test` to execute the unit tests via [Karma](https://karma-runner.github.io). 13 | 14 | ## App Description 15 | Angular demo app with examples of form creation, validation, class inheritance, etc. 16 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | require('karma-spec-reporter'), 15 | //require('karma-phantomjs-launcher'), 16 | require('karma-junit-reporter') 17 | ], 18 | client: { 19 | clearContext: false // leave Jasmine Spec Runner output visible in browser 20 | }, 21 | files: [ 22 | 23 | ], 24 | preprocessors: { 25 | 26 | }, 27 | mime: { 28 | 'text/x-typescript': ['ts', 'tsx'] 29 | }, 30 | coverageIstanbulReporter: { 31 | dir: require('path').join(__dirname, 'coverage'), reports: ['html', 'lcovonly'], 32 | fixWebpackSourcePaths: true 33 | }, 34 | junitReported: { 35 | outputDir: '', // results will be saved as $outputDir/$browserName.xml 36 | outputFile: 'test.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile 37 | }, 38 | angularCli: { 39 | environment: 'dev' 40 | }, 41 | // reporters: config.angularCli && config.angularCli.codeCoverage 42 | // ? ['spec', 'coverage-istanbul'] 43 | // : ['spec'], 44 | reporters: config.angularCli && config.angularCli.codeCoverage 45 | ? ['progress', 'coverage-istanbul', 'junit'] 46 | : ['progress', 'kjhtml', 'junit'], 47 | port: 9876, 48 | colors: true, 49 | logLevel: config.LOG_INFO, 50 | autoWatch: true, 51 | customLaunchers: { 52 | ChromeHeadless: { 53 | base: 'Chrome', 54 | flags: [ 55 | '--headless', 56 | '--disable-gpu', 57 | '--no-sandbox', 58 | '--remote-debugging-port=9222', 59 | ] 60 | } 61 | }, 62 | browsers: ['Chrome', 'ChromeHeadless'], 63 | singleRun: false, 64 | sourcemaps: false, 65 | browserNoActivityTimeout: 60000, //default 10000 66 | browserDisconnectTimeout: 10000, // default 2000 67 | browserDisconnectTolerance: 1, // default 0 68 | captureTimeout: 60000 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-patterns-demo", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "test": "ng test --browsers=Chrome", 9 | "test-cc": "ng test --browsers=Chrome --code-coverage", 10 | "test-headless": "ng test --browsers=ChromeHeadless --single-run", 11 | "test-coverage": "ng test --browsers=ChromeHeadless --single-run --code-coverage", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e", 14 | "build": "ng build", 15 | "dev-iis": "ng build --configuration=dev --build-optimizer --aot --output-hashing all", 16 | "deploy": "ng build --configuration=deploy --build-optimizer --aot --output-hashing all", 17 | "prod": "ng build --configuration=prod --build-optimizer --aot --output-hashing all" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "~8.2.11", 22 | "@angular/cdk": "~8.2.3", 23 | "@angular/common": "~8.2.11", 24 | "@angular/compiler": "~8.2.11", 25 | "@angular/core": "~8.2.11", 26 | "@angular/forms": "~8.2.11", 27 | "@angular/material": "^8.2.3", 28 | "@angular/platform-browser": "~8.2.11", 29 | "@angular/platform-browser-dynamic": "~8.2.11", 30 | "@angular/router": "~8.2.11", 31 | "hammerjs": "^2.0.8", 32 | "rxjs": "~6.4.0", 33 | "tslib": "^1.10.0", 34 | "zone.js": "~0.9.1", 35 | "adal-angular4": "~4.0.12", 36 | "@microsoft/applicationinsights-web": "~2.4.4", 37 | "bootstrap": "~3.4.1", 38 | "core-js": "2.5.4", 39 | "font-awesome": "4.7.0", 40 | "less": "3.0.4", 41 | "lodash": "~4.17.15", 42 | "moment": "2.22.1", 43 | "primeicons": "1.0.0", 44 | "primeng": "~7.0.0" 45 | }, 46 | "devDependencies": { 47 | "@angular-devkit/build-angular": "~0.803.14", 48 | "@angular/cli": "~8.3.14", 49 | "@angular/compiler-cli": "~8.2.11", 50 | "@angular/language-service": "~8.2.11", 51 | "@types/node": "~8.9.4", 52 | "@types/jasmine": "~3.3.8", 53 | "@types/jasminewd2": "~2.0.3", 54 | "@types/adal-angular": "~1.0.1", 55 | "@types/lodash": "~4.14.144", 56 | "codelyzer": "^5.0.0", 57 | "jasmine-core": "~3.4.0", 58 | "jasmine-spec-reporter": "~4.2.1", 59 | "karma": "~4.1.0", 60 | "karma-chrome-launcher": "~2.2.0", 61 | "karma-coverage-istanbul-reporter": "~2.0.1", 62 | "karma-jasmine": "~2.0.1", 63 | "karma-jasmine-html-reporter": "^1.4.0", 64 | "karma-junit-reporter": "~2.0.1", 65 | "karma-spec-reporter": "~0.0.32", 66 | "protractor": "~5.4.0", 67 | "ts-node": "~7.0.0", 68 | "tslint": "~5.15.0", 69 | "typescript": "~3.5.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | beforeLaunch: function() { 24 | require('ts-node').register({ 25 | project: 'e2e/tsconfig.e2e.json' 26 | }); 27 | }, 28 | onPrepare: function() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/app-injector.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { AppInjector } from './app-injector.service'; 2 | import { inject, TestBed } from '@angular/core/testing'; 3 | 4 | describe('AppInjector', () => { 5 | beforeEach(() => { 6 | 7 | TestBed.configureTestingModule({ 8 | providers: [ 9 | AppInjector 10 | ] 11 | }); 12 | }); 13 | 14 | it('should only create one instance', () => { 15 | const service1 = AppInjector.getInstance(); 16 | const service2 = AppInjector.getInstance(); 17 | expect(service1).toBe(service2); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/app-injector.service.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | 3 | export class AppInjector { 4 | private static instance: AppInjector; 5 | private injector: Injector; 6 | 7 | static getInstance() { 8 | if (!AppInjector.instance) { 9 | AppInjector.instance = new AppInjector(); 10 | } 11 | 12 | return AppInjector.instance; 13 | } 14 | 15 | private constructor() {} 16 | 17 | setInjector(injector: Injector) { 18 | this.injector = injector; 19 | } 20 | 21 | getInjector(): Injector { 22 | return this.injector; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuardService } from './framework/services/auth-guard.service'; 4 | import { HomeComponent } from './home/home.component'; 5 | import { PageNotFoundComponent } from './home/page-not-found/page-not-found.component'; 6 | import { PageErrorComponent } from './home/page-error/page-error.component'; 7 | import { LogOutComponent } from './home/log-out/log-out.component'; 8 | 9 | const routes: Routes = [ 10 | { path: 'home', component: HomeComponent, canActivate: [AuthGuardService] }, 11 | { path: 'error', component: PageErrorComponent }, 12 | { path: 'logout', component: LogOutComponent }, 13 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 14 | { path: '**', component: PageNotFoundComponent } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule {} 22 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '../scss/colors'; 2 | @import '../scss/mixins'; 3 | 4 | .overlay.loading { 5 | position:fixed; 6 | z-index: 999; 7 | top:0; 8 | left:0; 9 | width: 100%; 10 | height: 100%; 11 | opacity: .7; 12 | background-color: $text-med; 13 | } 14 | .hide-overflow { 15 | width: 100%; 16 | overflow-x: hidden; 17 | min-height: 93vh; 18 | } 19 | .container-fluid { 20 | margin-top: 80px; 21 | padding-top: 20px; 22 | margin-bottom: 30px; 23 | } 24 | .loadingGif { 25 | position: fixed; 26 | z-index: 1000; 27 | overflow: show; 28 | margin: auto; 29 | top: 0; 30 | left: 0; 31 | bottom: 0; 32 | right: 0; 33 | height: 4em; 34 | width: 4em; 35 | } 36 | .timeoutWarning { 37 | padding-top: 50px; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IAppConfig } from './demo-common/models/app-config.model'; 3 | import { environment } from 'environments/environment'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { IAppMonitor } from './shared/models/app-monitor'; 6 | 7 | @Injectable() 8 | export class AppConfig { 9 | 10 | static settings: IAppConfig; 11 | static appMonitor: IAppMonitor; 12 | 13 | constructor(private http: HttpClient) { 14 | } 15 | 16 | load() { 17 | const cacheBusterParam = (new Date()).getTime(); 18 | const jsonFile = `assets/config/config.${environment.name}.json?nocache=${cacheBusterParam}`; 19 | return new Promise((resolve, reject) => { 20 | this.http.get(jsonFile).toPromise() 21 | .then((response: IAppConfig) => { 22 | AppConfig.settings = response; 23 | resolve(); 24 | }).catch((response: any) => { 25 | reject(`Could not load file '${jsonFile}': ${JSON.stringify(response)}`); 26 | }); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 2 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 3 | import { UrlSerializer } from '@angular/router'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 6 | import { ApplicationInsights } from '@microsoft/applicationinsights-web'; 7 | import { DemoCommonModule } from './demo-common/demo-common.module'; 8 | import { FrameworkModule } from './framework/framework.module'; 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { DemoModule } from './demo/demo.module'; 11 | import { LowerCaseUrlSerializer } from './framework/services/url-serializer.service'; 12 | import { AppComponent } from './app.component'; 13 | import { HomeComponent } from './home/home.component'; 14 | import { PageNotFoundComponent } from './home/page-not-found/page-not-found.component'; 15 | import { PageErrorComponent } from './home/page-error/page-error.component'; 16 | import { LogOutComponent } from './home/log-out/log-out.component'; 17 | import { AppConfig } from './app.config'; 18 | import { AuthInterceptorService } from './framework/services/auth-interceptor.service'; 19 | 20 | export function initializeApp(appConfig: AppConfig) { 21 | const promise = appConfig.load().then(() => { 22 | if (AppConfig.settings && AppConfig.settings.logging && 23 | AppConfig.settings.logging.appInsights) { 24 | const appInsights = new ApplicationInsights({ 25 | config: { 26 | instrumentationKey: AppConfig.settings.appInsights.instrumentationKey, 27 | enableAutoRouteTracking: true // option to log all route changes 28 | } 29 | }); 30 | appInsights.loadAppInsights(); 31 | appInsights.trackPageView(); 32 | AppConfig.appMonitor = appInsights; 33 | } 34 | }); 35 | return () => promise; 36 | } 37 | 38 | @NgModule({ 39 | imports: [ 40 | BrowserModule, 41 | FrameworkModule, 42 | BrowserAnimationsModule, 43 | DemoCommonModule, 44 | DemoModule, 45 | AppRoutingModule 46 | ], 47 | declarations: [ 48 | AppComponent, 49 | HomeComponent, 50 | PageNotFoundComponent, 51 | PageErrorComponent, 52 | LogOutComponent 53 | ], 54 | providers: [ 55 | AppConfig, 56 | { provide: APP_INITIALIZER, useFactory: initializeApp, deps: [AppConfig], multi: true }, 57 | { provide: UrlSerializer, useClass: LowerCaseUrlSerializer }, 58 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true }, 59 | ], 60 | bootstrap: [ 61 | AppComponent 62 | ] 63 | }) 64 | export class AppModule { } 65 | -------------------------------------------------------------------------------- /src/app/demo-common/demo-common.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { HeaderBarComponent } from './directives/header-bar/header-bar.component'; 5 | import { TopNavComponent } from './directives/top-nav/top-nav.component'; 6 | import { TransactionDataService } from './services/transaction-data.service'; 7 | import { TransactionService } from './services/transaction.service'; 8 | import { TransactionEntityDataService } from './services/transaction-entity-data.service'; 9 | import { SearchDataService } from './services/search-data.service'; 10 | import { SearchService } from './services/search.service'; 11 | import { UserSessionService } from './services/user-session.service'; 12 | import { ComponentHeaderComponent } from './directives/component-header/component-header.component'; 13 | import { DemoCommonDataService } from './services/demo-common-data.service'; 14 | import { TransactionConfirmDialogComponent } from './directives/transaction-confirm-dialog/transaction-confirm-dialog.component'; 15 | import { SharedModule } from '../shared/shared.module'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | RouterModule, 21 | SharedModule 22 | ], 23 | declarations: [ 24 | TopNavComponent, 25 | HeaderBarComponent, 26 | TransactionConfirmDialogComponent, 27 | ComponentHeaderComponent 28 | ], 29 | providers: [ 30 | UserSessionService, 31 | DemoCommonDataService, 32 | TransactionEntityDataService, 33 | TransactionDataService, 34 | TransactionService, 35 | SearchDataService, 36 | SearchService 37 | ], 38 | exports: [ 39 | TopNavComponent, 40 | HeaderBarComponent, 41 | TransactionConfirmDialogComponent, 42 | ComponentHeaderComponent 43 | ] 44 | }) 45 | export class DemoCommonModule { 46 | } 47 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/component-header/component-header.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 |
7 |
8 | 14 |
15 |

{{title}}

16 |
17 |
18 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/component-header/component-header.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | 4 | @include generic-button(); 5 | 6 | .save-button { 7 | margin-left:4px; 8 | } 9 | 10 | .row { 11 | margin-bottom: 1em; 12 | margin-top: 3em; 13 | } 14 | 15 | .header { 16 | @include font-h(); 17 | margin-top: -20px; 18 | } 19 | 20 | h2, .h2 { 21 | font-family: Tahoma, Helvetica, Arial, sans-serif; 22 | font-weight: 520; 23 | font-size: 21px; 24 | // margin-top: 0px; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/component-header/component-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentHeaderComponent } from './component-header.component'; 2 | import { TestInjector } from '../../testing/testing-helpers'; 3 | 4 | describe('ComponentHeaderComponent', () => { 5 | let component: ComponentHeaderComponent; 6 | beforeAll(() => { 7 | TestInjector.setInjector(); 8 | }); 9 | 10 | beforeEach(() => { 11 | component = new ComponentHeaderComponent(); 12 | }); 13 | 14 | it('should be created', () => { 15 | expect(component).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/component-header/component-header.component.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, AbstractControl } from '@angular/forms'; 2 | import { Component, Output, Input, EventEmitter } from '@angular/core'; 3 | import { ActionCode } from '../../../framework/models/authorization.types'; 4 | 5 | @Component({ 6 | selector: 'la-component-header', 7 | templateUrl: './component-header.component.html', 8 | styleUrls: ['./component-header.component.scss'] 9 | }) 10 | export class ComponentHeaderComponent { 11 | 12 | @Input() title: string; 13 | @Input() isEditable = false; 14 | @Input() disabled: boolean; 15 | @Input() form: FormGroup; 16 | @Input() updatePermission: ActionCode = 'UPDATE'; 17 | @Input() customInvalid = false; 18 | @Output() toggleEdit = new EventEmitter(); 19 | @Output() onSave = new EventEmitter(); 20 | isAccountReadOnly = false; 21 | 22 | isEditDisabled(): boolean { 23 | if (this.disabled) { 24 | return true; 25 | } else { 26 | return this.isAccountReadOnly; 27 | } 28 | } 29 | 30 | toggle() { 31 | this.toggleEdit.emit(!this.isEditable); 32 | } 33 | 34 | save() { 35 | this.onSave.emit(); 36 | } 37 | 38 | formInvalid() { 39 | if (!this.form || !this.form.invalid) { 40 | return false; 41 | } 42 | 43 | let invalid = false; // default to valid 44 | 45 | Object.keys(this.form.controls).forEach(fieldName => { 46 | if (this.form.controls[fieldName] instanceof FormGroup) { 47 | Object.keys((this.form.controls[fieldName]).controls).forEach(fldName => { 48 | if (this.anyControlsInvalid((this.form.controls[fieldName]).controls[fldName])) { 49 | invalid = true; 50 | } 51 | }); 52 | } else { 53 | if (this.anyControlsInvalid(this.form.controls[fieldName])) { 54 | invalid = true; 55 | } 56 | } 57 | 58 | }); 59 | return invalid; 60 | } 61 | 62 | private anyControlsInvalid(control: AbstractControl) { 63 | let invalidFound = false; 64 | // check if the only errors also are listed as warnings 65 | if (control.errors) { 66 | Object.keys(control.errors).forEach(errorKey => { 67 | // If any errors on this field and none of them are warning or 68 | // one of the errors matches none of the warnings, then form is invalid 69 | if (!(control).warnings || 70 | !((control).warnings.includes(errorKey))) { 71 | invalidFound = true; 72 | } 73 | }); 74 | } 75 | return invalidFound; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/header-bar/header-bar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 21 |
22 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/header-bar/header-bar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | 4 | .navbar { 5 | min-height: 60px !important; 6 | max-height: 60px; 7 | z-index: 1001; 8 | background-color: #004059; 9 | border: none; 10 | 11 | min-width: 768px; 12 | } 13 | .navbar-brand { 14 | margin-left: 0 !important; 15 | margin-right: 50px; 16 | padding: 0; 17 | line-height: 48px; 18 | } 19 | .navbar-brand img { 20 | height: 44px; 21 | display: inline; 22 | border-right: 1px solid rgb(44, 133, 169); 23 | padding-left: 10px; 24 | padding-right: 30px; 25 | margin-top: 5px; 26 | } 27 | .navbar-brand img + span { 28 | padding-left: 10px; 29 | } 30 | .fa, .glyphicon, li { 31 | color: #f5f5f5; 32 | font-size: 1em; 33 | cursor: pointer; 34 | } 35 | li button { 36 | padding: 15px; 37 | color: #000; 38 | } 39 | 40 | .nav > li > a { 41 | padding: 20px; 42 | } 43 | .nav > li > a:hover, .nav > li > a:active { 44 | color: #fff; 45 | } 46 | .headerSearch { 47 | padding-top: 10px; 48 | } 49 | .closeSearch { 50 | margin-top: 5px; 51 | } 52 | .closeSearch a { 53 | padding-bottom: 35px !important; 54 | } 55 | 56 | .font-light { 57 | font-weight: 100; 58 | } 59 | 60 | .page-title { 61 | @include font-page-title(); 62 | position: relative; 63 | top: 7px; 64 | letter-spacing: .25px; 65 | white-space: nowrap; 66 | overflow: hidden; 67 | text-overflow: ellipsis; 68 | } 69 | 70 | .fa-user { 71 | margin-right: 4px; 72 | } 73 | 74 | body .ui-menu .ui-menu-list .ui-menuitem .ui-menuitem-link { 75 | @include link-logout(); 76 | } 77 | 78 | header > nav > ul > li { 79 | position: fixed; 80 | top: 0; 81 | right: 0; 82 | } 83 | 84 | div.navbar-header > button { 85 | display: none !important; 86 | } -------------------------------------------------------------------------------- /src/app/demo-common/directives/header-bar/header-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { AuthService } from '../../../framework/services/auth.service'; 3 | import { HeaderBarComponent } from './header-bar.component'; 4 | import { TestInjector } from '../../testing/testing-helpers'; 5 | import { MenuComponent } from '../../../shared/directives/menu/menu.component'; 6 | 7 | describe('HeaderBarComponent', () => { 8 | let component: HeaderBarComponent; 9 | beforeAll(() => { 10 | TestInjector.setInjector(); 11 | }); 12 | 13 | beforeEach(() => { 14 | component = new HeaderBarComponent( 15 | TestInjector.getService(AuthService), 16 | TestInjector.getService(Router)); 17 | component.userMenu = new MenuComponent(null); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/header-bar/header-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MenuItem } from 'primeng/primeng'; 4 | import { AuthService } from '../../../framework/services/auth.service'; 5 | import { MenuComponent } from '../../../shared/directives/menu/menu.component'; 6 | 7 | @Component({ 8 | selector: 'la-header-bar', 9 | templateUrl: './header-bar.component.html', 10 | styleUrls: ['./header-bar.component.scss'] 11 | }) 12 | export class HeaderBarComponent { 13 | showSearch = false; 14 | userMenuItems: Array = [ 15 | { 16 | label: 'logout', 17 | command: () => { this.signOut(); } 18 | } 19 | ]; 20 | @ViewChild('userMenu', { static: false }) userMenu: MenuComponent; 21 | 22 | constructor(private authService: AuthService, private router: Router) { 23 | } 24 | 25 | toggleUserMenu(event) { 26 | this.userMenu.toggle(event); 27 | window.scrollTo(0, 0); 28 | } 29 | 30 | loggedIn() { 31 | return this.authService.isUserAuthenticated; 32 | } 33 | 34 | userName() { 35 | return this.authService.userName; 36 | } 37 | 38 | signIn() { 39 | this.authService.login(); 40 | } 41 | 42 | signOut() { 43 | this.router.navigate(['/logout']); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/top-nav/top-nav.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/top-nav/top-nav.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | 4 | .navbar { 5 | min-height: auto !important; 6 | } 7 | 8 | #top-nav { 9 | position: fixed; 10 | top: 60px; 11 | width: 100%; 12 | z-index: 900; 13 | 14 | background-color: $nav-blue !important; 15 | padding-left: 0 !important; 16 | padding-right: 0 !important; 17 | 18 | font-size: 1em; 19 | max-height: 40px; 20 | min-width: 992px; 21 | } 22 | 23 | #pn-main-nav { 24 | background-color: $nav-blue; 25 | @include font-body(); 26 | 27 | max-height: 40px; 28 | } 29 | 30 | div#pn-main-nav { 31 | ul { 32 | margin: 0 !important; 33 | max-height: 40px; 34 | li { 35 | max-height: 40px; 36 | a { 37 | height: 40px; 38 | padding: 10px 15px; 39 | } 40 | } 41 | } 42 | } 43 | 44 | div#pn-main-nav > ul > li > a { 45 | color: $canvas; 46 | } 47 | 48 | div#pn-main-nav > ul > li:active > a { 49 | color: $nav-blue; 50 | background-color: $nav-active; 51 | } 52 | .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { 53 | background-color: $nav-hover; 54 | } 55 | 56 | div#pn-main-nav > ul > li > a:hover { 57 | color: $canvas; 58 | background-color: $nav-hover; 59 | } 60 | 61 | .navbar-default .navbar-collapse, .navbar-default .navbar-form { 62 | margin-bottom: -1.5px; 63 | } 64 | 65 | .ui-menubar .ui-menubar-root-list { 66 | height:50px; 67 | } 68 | 69 | .ui-menu.ui-menubar .ui-menubar-root-list > li > a > .ui-submenu-icon, body .ui-menu.ui-menubar > .ui-menu-list > .ui-menuitem > .ui-menuitem-link { 70 | height:50px; 71 | } 72 | 73 | .ui-menu .ui-menuitem-text, .ui-menu .ui-widget-header h1, .ui-menu .ui-widget-header h2, .ui-menu .ui-widget-header h3, .ui-menu .ui-widget-header h4, .ui-menu .ui-widget-header h5, .ui-menu .ui-widget-header h6 { 74 | @include link-menu(); 75 | 76 | } 77 | 78 | a.ui-submenu-link { 79 | .ui-menuitem-text { 80 | font-weight: bold; 81 | color: white !important; 82 | 83 | } 84 | } 85 | 86 | body .ui-state-disabled, body .ui-widget:disabled { 87 | &:hover { 88 | cursor: not-allowed !important; 89 | } 90 | span { 91 | &:hover { 92 | cursor: not-allowed !important; 93 | } 94 | } 95 | } 96 | 97 | .link-menu { 98 | @include link-menu(); 99 | color: white !important; 100 | 101 | &:hover { 102 | background-color: $link-blue !important; 103 | } 104 | } 105 | 106 | #pn-main-nav > ul > li a, 107 | #pn-main-nav > ul > li a span { 108 | text-transform: capitalize; 109 | } 110 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/top-nav/top-nav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TopNavComponent } from './top-nav.component'; 2 | import { TestInjector } from '../../testing/testing-helpers'; 3 | 4 | describe('TopNavComponent', () => { 5 | let component: TopNavComponent; 6 | beforeAll(() => { 7 | TestInjector.setInjector(); 8 | }); 9 | 10 | beforeEach(() => { 11 | component = new TopNavComponent(); 12 | }); 13 | 14 | it('should create', () => { 15 | expect(component).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/top-nav/top-nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-top-nav', 5 | templateUrl: './top-nav.component.html', 6 | styleUrls: ['./top-nav.component.scss'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class TopNavComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/transaction-confirm-dialog/transaction-confirm-dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/demo-common/directives/transaction-confirm-dialog/transaction-confirm-dialog.component.css -------------------------------------------------------------------------------- /src/app/demo-common/directives/transaction-confirm-dialog/transaction-confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

{{message}}

3 |
4 |
5 | 6 | 8 |
9 |
10 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/transaction-confirm-dialog/transaction-confirm-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TransactionConfirmDialogComponent } from './transaction-confirm-dialog.component'; 2 | import { TestInjector } from '../../testing/testing-helpers'; 3 | 4 | describe('TransactionConfirmDialogComponent', () => { 5 | let component: TransactionConfirmDialogComponent; 6 | beforeAll(() => { 7 | TestInjector.setInjector(); 8 | }); 9 | 10 | beforeEach(() => { 11 | component = new TransactionConfirmDialogComponent(); 12 | }); 13 | 14 | it('should create', () => { 15 | expect(component).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/demo-common/directives/transaction-confirm-dialog/transaction-confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, Input, EventEmitter, Output } from '@angular/core'; 2 | import { ConfirmDialogComponent } from '../../../shared/directives/confirm-dialog/confirm-dialog.component'; 3 | import { ConfirmChoice } from '../../../shared/models/confirm-choices.enum'; 4 | 5 | @Component({ 6 | selector: 'la-transaction-confirm-dialog', 7 | templateUrl: './transaction-confirm-dialog.component.html', 8 | styleUrls: ['./transaction-confirm-dialog.component.css'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class TransactionConfirmDialogComponent extends ConfirmDialogComponent { 12 | description = ''; 13 | @Input() confirm = false; 14 | @Input() allowDiscard = true; 15 | @Output() confirmed: EventEmitter = new EventEmitter(); 16 | 17 | saveButtonText() { 18 | if (this.allowSave && this.confirm) { 19 | return 'Save and commit'; 20 | } else if (this.allowSave) { 21 | return 'Save'; 22 | } else if (this.confirm) { 23 | return 'Commit'; 24 | } 25 | return ''; 26 | } 27 | 28 | discard() { 29 | this.display = false; 30 | this.confirmed.emit(ConfirmChoice.discard); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/demo-common/models/account-search-result.models.ts: -------------------------------------------------------------------------------- 1 | export interface IAccountSearchResult { 2 | accountCode: string; 3 | accountName: string; 4 | accountType: string; 5 | accountStatus: number; 6 | accountStatusDisplay: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/demo-common/models/app-config.model.ts: -------------------------------------------------------------------------------- 1 | export interface IAppConfig { 2 | env: { 3 | name: string; 4 | }; 5 | appInsights: { 6 | instrumentationKey: string; 7 | }; 8 | logging: { 9 | console: boolean; 10 | appInsights: boolean; 11 | traceEnabled: boolean; 12 | }; 13 | aad: { 14 | requireAuth: boolean; 15 | tenant: string; 16 | resource: string; 17 | clientId: string; 18 | endpoints: { [key: string]: string }; 19 | }; 20 | apiServer: { 21 | metadata: string; 22 | rules: string; 23 | transaction: string; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/demo-common/models/transaction.models.ts: -------------------------------------------------------------------------------- 1 | export interface IAccountTransactionObject { 2 | accountCode: string; 3 | accountName: string; 4 | transactionId: number; 5 | description: string; 6 | } 7 | 8 | export interface INewTransaction { 9 | accountCode: string; 10 | description: string; 11 | } 12 | 13 | export interface ITransactionIdentifier { 14 | id: number; 15 | } 16 | 17 | export interface IEntity { 18 | transactionIdentifier: ITransactionIdentifier; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/demo-common/services/demo-common-data.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { AppInjector } from '../../app-injector.service'; 3 | import { Injectable } from '@angular/core'; 4 | import { UserSessionService } from './user-session.service'; 5 | import { GlobalEventsService } from '../../framework/services/global-events.service'; 6 | import { LoggingService } from '../../framework/logging/logging.service'; 7 | import { UtilitiesService } from '../../framework/services/utilities.service'; 8 | import { AuthService } from '../../framework/services/auth.service'; 9 | import { DataService } from '../../framework/services/data.service'; 10 | 11 | @Injectable() 12 | export class DemoCommonDataService extends DataService { 13 | 14 | // Add any dependencies that are in demo-common, but not in the framework 15 | protected userSessionService: UserSessionService; 16 | 17 | constructor() { 18 | // Manually retrieve the dependencies from the injector 19 | // so that constructor has no dependencies that need to be passed in from child 20 | const injector = AppInjector.getInstance().getInjector(); 21 | const http = injector.get(HttpClient); 22 | const authService = injector.get(AuthService); 23 | const utilitiesService = injector.get(UtilitiesService); 24 | const loggingService = injector.get(LoggingService); 25 | const globalEventsService = injector.get(GlobalEventsService); 26 | 27 | super(http, authService, utilitiesService, loggingService, globalEventsService); 28 | 29 | this.userSessionService = injector.get(UserSessionService); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/demo-common/services/demo-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Router } from '@angular/router'; 3 | import { AppInjector } from '../../app-injector.service'; 4 | import { UtilitiesService } from '../../framework/services/utilities.service'; 5 | import { UserSessionService } from './user-session.service'; 6 | import { GlobalEventsService } from '../../framework/services/global-events.service'; 7 | import { IServerError } from '../../framework/validation/models/server-error.models'; 8 | import { SystemMessageService } from '../../framework/services/system-message.service'; 9 | import { TransactionService } from './transaction.service'; 10 | 11 | @Injectable() 12 | export class DemoResolver { 13 | 14 | protected router: Router; 15 | protected userSessionService: UserSessionService; 16 | protected utilitiesService: UtilitiesService; 17 | protected globalEventsService: GlobalEventsService; 18 | protected systemMessageService: SystemMessageService; 19 | protected transactionService: TransactionService; 20 | private navigatingToUrl: string; 21 | 22 | constructor() { 23 | // Manually retrieve the dependencies from the injector 24 | // so that constructor has no dependencies that need to be passed in from child 25 | const injector = AppInjector.getInstance().getInjector(); 26 | this.router = injector.get(Router); 27 | this.utilitiesService = injector.get(UtilitiesService); 28 | this.userSessionService = injector.get(UserSessionService); 29 | this.globalEventsService = injector.get(GlobalEventsService); 30 | this.systemMessageService = injector.get(SystemMessageService); 31 | this.transactionService = injector.get(TransactionService); 32 | } 33 | 34 | resolve(route: ActivatedRouteSnapshot) { 35 | this.navigatingToUrl = (route)._routerState ? (route)._routerState.url : ''; 36 | this.globalEventsService.startRouting(); 37 | return null; 38 | } 39 | 40 | routeToNavigationErrorPage(error?: string | IServerError) { 41 | this.userSessionService.navigationError = { 42 | navigatingTo: this.navigatingToUrl 43 | }; 44 | if (typeof error === 'string') { 45 | this.userSessionService.navigationError.message = error; 46 | } else { 47 | this.userSessionService.navigationError.serverError = error; 48 | } 49 | 50 | if (this.userSessionService.accountCode) { 51 | // Append a random number so that the routing will redirect even if already on the 52 | // navigation-error page 53 | const randomNumber = Math.floor(Math.random() * 1000000000); 54 | this.router.navigate([ 55 | `/accounts/${this.userSessionService.accountCode}/navigation-error/${randomNumber}`]); 56 | } else { 57 | this.router.navigate(['accounts/searchresults']); 58 | } 59 | return Promise.resolve(null); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/demo-common/services/search-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IAccountSearchResult } from '../models/account-search-result.models'; 3 | import { DemoCommonDataService } from './demo-common-data.service'; 4 | 5 | @Injectable() 6 | export class SearchDataService extends DemoCommonDataService { 7 | 8 | getAccountSearchResults() { 9 | const endpoint = `${this.apiServer.rules}accounts/`; 10 | return this.get>(endpoint); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/demo-common/services/search.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject, async } from '@angular/core/testing'; 2 | import { IAccountSearchResult } from '../models/account-search-result.models'; 3 | import { SearchDataService } from './search-data.service'; 4 | import { SearchService } from './search.service'; 5 | 6 | const searchResults = [{ 7 | accountCode: 'ABC123', 8 | accountName: 'Test Account 1', 9 | accountType: '' 10 | }, 11 | { 12 | accountCode: 'ABC234', 13 | accountName: 'Test Account 2', 14 | accountType: '' 15 | } 16 | ]; 17 | 18 | class MockSearchDataService { 19 | getAccountSearchResults(showInactive?: boolean): Promise> { 20 | return new Promise>((resolve) => { 21 | resolve(>searchResults); 22 | }); 23 | } 24 | } 25 | 26 | describe('SearchService', () => { 27 | 28 | let service: SearchService; 29 | 30 | beforeEach(() => { 31 | TestBed.configureTestingModule({ 32 | providers: [ 33 | SearchService, 34 | { 35 | provide: SearchDataService, 36 | useClass: MockSearchDataService 37 | } 38 | ] 39 | }); 40 | }); 41 | 42 | beforeEach(inject([SearchService], (s: SearchService) => { 43 | service = s; 44 | })); 45 | 46 | it('should create the service', () => { 47 | expect(service).toBeTruthy(); 48 | }); 49 | 50 | it('can get accounts', async(() => { 51 | service.getAccounts().then(accounts => { 52 | expect(accounts.length).toBe(2); 53 | }).catch(error => { 54 | expect(error).toBeNull(); 55 | }); 56 | })); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app/demo-common/services/search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IAccountSearchResult } from '../models/account-search-result.models'; 3 | import { SearchDataService } from './search-data.service'; 4 | import { IChild1Entity } from '../../demo/accounts/shared/models/child1-entity.models'; 5 | 6 | @Injectable() 7 | export class SearchService { 8 | 9 | private accounts: Array = []; 10 | 11 | constructor(private searchDataService: SearchDataService) { 12 | this.accounts = []; 13 | } 14 | 15 | getAccounts() { 16 | return this._getAccounts(); 17 | } 18 | 19 | accountUpdated(child1: IChild1Entity) { 20 | this.updateList(this.accounts, child1); 21 | } 22 | 23 | private updateList(accountList: IAccountSearchResult[], child1: IChild1Entity) { 24 | // Find the saved account in the search results list 25 | const index = accountList.findIndex(p => { 26 | return p.accountCode === child1.accountCode; 27 | }); 28 | 29 | // If found, update the name if that was changed 30 | if (index !== -1) { 31 | accountList[index].accountName = child1.name1; 32 | } 33 | } 34 | 35 | private _getAccounts() { 36 | const promise = new Promise((resolve, reject) => { 37 | if (this.accounts.length > 0) { 38 | resolve(this.accounts); 39 | } else { 40 | this.searchDataService.getAccountSearchResults() 41 | .then(accounts => { 42 | accounts.forEach(item => { 43 | item.accountStatusDisplay = item.accountStatus === 1 ? 'Active' : 'Inactive'; 44 | }); 45 | this.accounts = accounts; 46 | resolve(accounts); 47 | }).catch((e) => { 48 | reject(e); 49 | }); 50 | } 51 | }); 52 | return promise; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/demo-common/services/transaction-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { INewTransaction, IAccountTransactionObject, ITransactionIdentifier } from '../models/transaction.models'; 3 | import { DemoCommonDataService } from './demo-common-data.service'; 4 | 5 | @Injectable() 6 | export class TransactionDataService extends DemoCommonDataService { 7 | 8 | getTransactionObject() { 9 | const endpoint = `${this.apiServer.rules}accounts/${this.userSessionService.accountCode.toUpperCase()}/transactionobject`; 10 | 11 | const promise = new Promise((resolve, reject) => { 12 | this.get(endpoint).then((transactionObject) => { 13 | resolve(transactionObject); 14 | }).catch((error) => { 15 | reject(error); 16 | }); 17 | }); 18 | return promise; 19 | 20 | } 21 | 22 | createTransaction(transaction: INewTransaction): Promise { 23 | const endpoint = `${this.apiServer.transaction}transactions`; 24 | return >(this.post(endpoint, transaction)); 25 | } 26 | 27 | commitTransaction(transactionId: number, description: string): Promise { 28 | const endpoint = `${this.apiServer.transaction}transactions/commit`; 29 | return >(this.post(endpoint, 30 | { 31 | transactionId: transactionId, 32 | description: description 33 | })); 34 | } 35 | 36 | rollbackTransaction(transactionId: number): Promise { 37 | const endpoint = `${this.apiServer.transaction}transactions/${transactionId}/rollback`; 38 | return >(this.post(endpoint, null)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/demo-common/services/transaction-entity-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AppInjector } from '../../app-injector.service'; 3 | import { TransactionService } from './transaction.service'; 4 | import { IEntity } from '../models/transaction.models'; 5 | import { DemoCommonDataService } from './demo-common-data.service'; 6 | 7 | @Injectable() 8 | export class TransactionEntityDataService extends DemoCommonDataService { 9 | 10 | protected transactionService: TransactionService; 11 | protected get accountUrlPrefix() { 12 | return `${this.apiServer.rules}accounts/${this.userSessionService.accountCode.toUpperCase()}`; 13 | } 14 | 15 | constructor() { 16 | super(); 17 | 18 | // Manually retrieve the dependencies from the injector 19 | // so that constructor has no dependencies that need to be passed in from child 20 | const injector = AppInjector.getInstance().getInjector(); 21 | this.transactionService = injector.get(TransactionService); 22 | } 23 | 24 | // This endpoint is used by the following: 25 | // - to call with 'getValidators=true' 26 | // - endpointWithTransactionId() 27 | protected constructEndpoint(route: string, additionalRouteParam?: string) { 28 | let endpoint = `${this.accountUrlPrefix}/${route}`; 29 | if (additionalRouteParam) { 30 | endpoint += `/${additionalRouteParam.toUpperCase()}`; 31 | } 32 | return endpoint; 33 | } 34 | 35 | addEnity(body: IEntity): Promise { 36 | const endpoint = this.endpoint(); 37 | return >this.post(endpoint, body); 38 | } 39 | 40 | updateEnity(body: IEntity, parameter?: string): Promise { 41 | const endpoint = this.endpoint(parameter); 42 | return this.put(endpoint, body); 43 | } 44 | 45 | updateEnityArray(body: IEntity[]): Promise { 46 | const endpoint = this.endpoint(); 47 | return this.put(endpoint, body); 48 | } 49 | 50 | endpointWithTransactionId(additionalRouteParam?: string) { 51 | let endpoint = this.endpoint(additionalRouteParam); 52 | 53 | if (this.transactionService.currentTransactionId()) { 54 | if (endpoint.indexOf('?') === -1) { 55 | endpoint += '?'; 56 | } else { 57 | endpoint += '&'; 58 | } 59 | endpoint += `transactionId=${this.transactionService.currentTransactionId().toString()}`; 60 | } 61 | 62 | return endpoint; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/demo-common/services/user-session.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { UserSessionService } from './user-session.service'; 3 | import { UtilitiesService } from '../../framework/services/utilities.service'; 4 | 5 | describe('UserSessionService', () => { 6 | 7 | let service: UserSessionService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | UserSessionService, 13 | UtilitiesService 14 | ] 15 | }); 16 | }); 17 | 18 | beforeEach(inject([UserSessionService], (s: UserSessionService) => { 19 | service = s; 20 | })); 21 | 22 | it('should create the service', () => { 23 | expect(service).toBeTruthy(); 24 | }); 25 | 26 | it('can get and set TransactionIdentifier', () => { 27 | service.transactionIdentifier = { 28 | id: 12345 29 | }; 30 | expect(service.transactionIdentifier.id).toBe(12345); 31 | }); 32 | 33 | it('can get and set account code', () => { 34 | service.accountCode = 'ABC123'; 35 | expect(service.accountCode).toBe('abc123'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/demo-common/services/user-session.service.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | import { UtilitiesService } from '../../framework/services/utilities.service'; 4 | import { ITransactionIdentifier } from '../models/transaction.models'; 5 | import { INavigationError } from '../../framework/validation/models/server-error.models'; 6 | 7 | @Injectable() 8 | export class UserSessionService { 9 | 10 | private _accountCode: string; 11 | private _accountType: string; 12 | 13 | private accountCodeChangedSource = new Subject(); 14 | accountCodeChanged = this.accountCodeChangedSource.asObservable(); 15 | 16 | transactionIdentifier: ITransactionIdentifier; 17 | navigationError: INavigationError; 18 | 19 | constructor(private utilitiesService: UtilitiesService) { 20 | this._accountCode = ''; 21 | this._accountType = ''; 22 | this.transactionIdentifier = null; 23 | } 24 | 25 | get accountCode() { 26 | return this._accountCode; 27 | } 28 | 29 | set accountCode(value: string) { 30 | value = value ? value.toLowerCase() : ''; 31 | this._accountCode = value; 32 | this.accountCodeChangedSource.next(); 33 | } 34 | 35 | get accountType() { 36 | return this._accountType; 37 | } 38 | 39 | set accountType(value: string) { 40 | this._accountType = value; 41 | } 42 | 43 | clearAccount() { 44 | this.accountType = ''; 45 | this.accountCode = ''; 46 | this.transactionIdentifier = null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component1/child-component1-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { MetadataService } from '../../../framework/services/metadata.service'; 4 | import { DemoAccountResolver } from '../shared/services/demo-account-resolver.service'; 5 | import { GlobalEventsService } from '../../../framework/services/global-events.service'; 6 | import { Child1DataService } from '../shared/services/child1-data.service'; 7 | import { IChild1Entity } from '../shared/models/child1-entity.models'; 8 | 9 | @Injectable() 10 | export class ChildComponent1Resolver extends DemoAccountResolver implements Resolve { 11 | 12 | constructor(private child1DataService: Child1DataService, 13 | private metadataService: MetadataService, 14 | protected globalEventsService: GlobalEventsService) { 15 | super(); 16 | } 17 | 18 | resolve(route: ActivatedRouteSnapshot) { 19 | super.resolve(route); 20 | const promise = new Promise((resolve, reject) => { 21 | this.child1DataService.getChild1() 22 | .then(child1 => { 23 | const p1 = this.metadataService.getLookupList('StatusCodes') 24 | .then(list => child1.statusCodeList = list); 25 | const p2 = this.metadataService.getLookupList('ProductCodes') 26 | .then(list => child1.productCodeList = list); 27 | const p3 = this.metadataService.getLookupList('StateCodes') 28 | .then(list => child1.stateCodeList = list); 29 | const p4 = this.metadataService.getLookupList('AccountTypes') 30 | .then(list => child1.accountTypeList = list); 31 | 32 | Promise.all([p1, p2, p3, p4]) 33 | .then(() => { 34 | resolve(child1); 35 | }).catch(error => { 36 | this.routeToNavigationErrorPage(error); 37 | resolve(null); 38 | }); 39 | }) 40 | .catch((error) => { 41 | this.routeToNavigationErrorPage(error); 42 | resolve(null); 43 | }); 44 | }); 45 | return promise; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component1/child-component1.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/demo/accounts/child-component1/child-component1.component.scss -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component1/child-component1.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { DemoTransactionComponent } from '../shared/components/demo-transaction-component'; 4 | import { SearchService } from '../../../demo-common/services/search.service'; 5 | import { AccountHeaderService } from '../shared/directives/account-header/account-header.service'; 6 | import { IChild1Entity } from '../shared/models/child1-entity.models'; 7 | import { Child1DataService } from '../shared/services/child1-data.service'; 8 | 9 | @Component({ 10 | selector: 'la-child1', 11 | templateUrl: './child-component1.component.html', 12 | styleUrls: ['./child-component1.component.scss'] 13 | }) 14 | export class ChildComponent1Component extends DemoTransactionComponent { 15 | 16 | child1: IChild1Entity; 17 | formControls = [ 18 | { name: 'accountCode' }, 19 | { name: 'name1' }, 20 | { name: 'name2' }, 21 | { name: 'name3' }, 22 | { name: 'addressLine1' }, 23 | { name: 'addressLine2' }, 24 | { name: 'addressLine3' }, 25 | { name: 'city' }, 26 | { name: 'state' }, 27 | { name: 'zip' }, 28 | { name: 'product' }, 29 | { name: 'accountType' }, 30 | { name: 'accountStatus' }, 31 | { name: 'startDate' }, 32 | { name: 'lastModifiedDate' } 33 | ]; 34 | protected routeParamName = 'child1'; 35 | private originalChild1: IChild1Entity; 36 | 37 | constructor(protected route: ActivatedRoute, 38 | private searchService: SearchService, 39 | private accountHeaderService: AccountHeaderService, 40 | private child1DataService: Child1DataService) { 41 | super(route); 42 | } 43 | 44 | protected get original() { 45 | return this.originalChild1; 46 | } 47 | 48 | protected set original(value: IChild1Entity) { 49 | this.originalChild1 = value; 50 | } 51 | 52 | protected get entity() { 53 | return this.child1; 54 | } 55 | 56 | saveComplete() { 57 | super.saveComplete(); 58 | this.accountHeaderService.updateAccountName( 59 | `${this.child1.name1} ${this.child1.name2} ${this.child1.name3}`); 60 | } 61 | 62 | commitComplete() { 63 | super.commitComplete(); 64 | // When an account is updated, we need to update the cached copy of the search results 65 | this.searchService.accountUpdated(this.child1); 66 | } 67 | 68 | protected set entity(value: IChild1Entity) { 69 | value.state = value.state.toUpperCase(); 70 | this.child1 = value; 71 | } 72 | 73 | protected get dataService() { 74 | return this.child1DataService; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component2/child-component2-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { DemoAccountResolver } from '../shared/services/demo-account-resolver.service'; 4 | import { MetadataService } from '../../../framework/services/metadata.service'; 5 | import { Child2DataService } from '../shared/services/child2-data.service'; 6 | import { IChild2Entity } from '../shared/models/child2-entity.models'; 7 | 8 | @Injectable() 9 | export class ChildComponent2Resolver extends DemoAccountResolver implements Resolve { 10 | 11 | constructor(private child2DataService: Child2DataService, 12 | private metadataService: MetadataService) { 13 | super(); 14 | } 15 | 16 | resolve(route: ActivatedRouteSnapshot) { 17 | super.resolve(route); 18 | 19 | const promise = new Promise((resolve, reject) => { 20 | this.child2DataService.getChild2() 21 | .then(child2 => { 22 | const p1 = this.metadataService.getLookupList('AccountTypes') 23 | .then(list => { 24 | child2.accountTypeList = list; 25 | }); 26 | Promise.all([p1]) 27 | .then(() => { 28 | resolve(child2); 29 | }).catch((error) => { 30 | this.routeToNavigationErrorPage(error); 31 | resolve(null); 32 | }); 33 | }) 34 | .catch((error) => { 35 | this.routeToNavigationErrorPage(error); 36 | resolve(null); 37 | }); 38 | }); 39 | return promise; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component2/child-component2.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 23 |
24 |
25 | 27 | 28 |
29 |
30 | 31 |

Contact Information

32 |
33 |
34 | 35 |
36 |
37 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component2/child-component2.component.scss: -------------------------------------------------------------------------------- 1 | pn-field.truncate-label > div > div > div.form-group > p.label { 2 | width: 80px; 3 | white-space: nowrap; 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component2/child-component2.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute } from '@angular/router'; 2 | import { async } from '@angular/core/testing'; 3 | import { of } from 'rxjs'; 4 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 5 | import { ChildComponent2Component } from './child-component2.component'; 6 | import { IChild2Entity } from '../shared/models/child2-entity.models'; 7 | 8 | const child2: IChild2Entity = { 9 | transactionIdentifier: null, 10 | name1: 'Account Name', 11 | name2: '', 12 | name3: '', 13 | accountType: 'Account Type', 14 | contactName: 'Contact Name', 15 | contactPhoneNumber: '123-1234', 16 | contactPhoneAreaCode: '303', 17 | rate: null, 18 | accountTypeList: [] 19 | }; 20 | 21 | class MockActivatedRoute extends ActivatedRoute { 22 | data = of({ 23 | child2: child2 24 | }); 25 | } 26 | 27 | const mockChild2DataService = jasmine.createSpyObj( 28 | 'mockChild2DataService', ['getChild2']); 29 | 30 | mockChild2DataService.getChild2.and.returnValue( 31 | new Promise((resolve) => { 32 | resolve(child2); 33 | })); 34 | 35 | describe('ChildComponent2Component', () => { 36 | let component: ChildComponent2Component; 37 | beforeAll(() => { 38 | TestInjector.setInjector(); 39 | }); 40 | 41 | beforeEach(() => { 42 | component = new ChildComponent2Component( 43 | new MockActivatedRoute(), 44 | mockChild2DataService); 45 | }); 46 | 47 | it('should be created', async(() => { 48 | component.componentLoadingComplete.subscribe(() => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | component.ngOnInit(); 52 | })); 53 | 54 | it('should toggle edit mode', async(() => { 55 | component.componentLoadingComplete.subscribe(() => { 56 | component.toggleEditMode(true); 57 | expect(component.editMode).toBe(true); 58 | }); 59 | component.ngOnInit(); 60 | })); 61 | 62 | it('should save the results', async(() => { 63 | component.componentLoadingComplete.subscribe(() => { 64 | component.toggleEditMode(true); 65 | component.form.get('contactName').setValue('John Doe'); 66 | component.save().then(() => { 67 | expect(component.child2.contactName).toBe('John Doe'); 68 | }); 69 | }); 70 | component.ngOnInit(); 71 | })); 72 | 73 | it('hasChanged is false if nothing is changed', async(() => { 74 | component.componentLoadingComplete.subscribe(() => { 75 | expect(component.hasChanged()).toBe(false); 76 | }); 77 | component.ngOnInit(); 78 | })); 79 | 80 | it('should set hasChanged to true if value is changed', async(() => { 81 | component.componentLoadingComplete.subscribe(() => { 82 | component.editMode = true; 83 | component.child2.rate = 50; 84 | expect(component.hasChanged()).toBe(true); 85 | }); 86 | component.ngOnInit(); 87 | })); 88 | }); 89 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component2/child-component2.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { IChild2Entity } from '../shared/models/child2-entity.models'; 4 | import { Child2DataService } from '../shared/services/child2-data.service'; 5 | import { IPartsFormControl } from '../../../framework/models/form-controls.models'; 6 | import { DemoTransactionComponent } from '../shared/components/demo-transaction-component'; 7 | 8 | @Component({ 9 | selector: 'la-child-component2', 10 | templateUrl: './child-component2.component.html', 11 | styleUrls: ['./child-component2.component.scss'] 12 | }) 13 | export class ChildComponent2Component extends DemoTransactionComponent { 14 | 15 | child2: IChild2Entity; 16 | originalChild2: IChild2Entity; 17 | protected routeParamName = 'child2'; 18 | formControls: Array = [ 19 | { name: 'name1' }, 20 | { name: 'name2' }, 21 | { name: 'name3' }, 22 | { name: 'contactName' }, 23 | { name: 'contactPhoneNumber' }, 24 | { name: 'contactPhoneAreaCode' }, 25 | { name: 'accountType' }, 26 | { name: 'rate' } 27 | ]; 28 | 29 | constructor(protected route: ActivatedRoute, 30 | private child2DataService: Child2DataService) { 31 | super(route); 32 | } 33 | 34 | protected get original() { 35 | return this.originalChild2; 36 | } 37 | 38 | protected set original(value: IChild2Entity) { 39 | this.originalChild2 = value; 40 | } 41 | 42 | protected get entity() { 43 | return this.child2; 44 | } 45 | 46 | protected set entity(value: IChild2Entity) { 47 | this.child2 = value; 48 | } 49 | 50 | protected get dataService() { 51 | return this.child2DataService; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component3/child-component3-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { MetadataService } from '../../../framework/services/metadata.service'; 4 | import { IChild3Entity } from '../shared/models/child3-entity.models'; 5 | import { Child3DataService } from '../shared/services/child3-data.service'; 6 | import { DemoAccountResolver } from '../shared/services/demo-account-resolver.service'; 7 | 8 | @Injectable() 9 | export class ChildComponent3Resolver extends DemoAccountResolver implements Resolve { 10 | 11 | constructor(private child3DataService: Child3DataService, 12 | private metadataService: MetadataService) { 13 | super(); 14 | } 15 | 16 | resolve(route: ActivatedRouteSnapshot) { 17 | 18 | super.resolve(route); 19 | 20 | const promise = new Promise((resolve, reject) => { 21 | return this.child3DataService.getChild3() 22 | .then(child3 => { 23 | return this.metadataService.getLookupList('TimePeriods') 24 | .then(list => { 25 | child3.timePeriodList = list; 26 | resolve(child3); 27 | }).catch((error) => { 28 | this.routeToNavigationErrorPage(error); 29 | resolve(null); 30 | }); 31 | }).catch((error) => { 32 | this.routeToNavigationErrorPage(error); 33 | resolve(null); 34 | }); 35 | }); 36 | return promise; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component3/child-component3.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/demo/accounts/child-component3/child-component3.component.scss -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component3/child-component3.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | import { ChildComponent3Component } from './child-component3.component'; 5 | import { UtilitiesService } from '../../../framework/services/utilities.service'; 6 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 7 | import { IChild3Entity } from '../shared/models/child3-entity.models'; 8 | 9 | const child3: IChild3Entity = { 10 | transactionIdentifier: { 11 | id: 0 12 | }, 13 | accountCode: '1', 14 | adminYearEndDate: new Date('2017-03-26'), 15 | statementYearEndDate: new Date('2017-03-26'), 16 | statementFrequencyPerYear: 365, 17 | processingFrequencyPerYear: 4, 18 | billingFrequencyPerYear: 4, 19 | adminFrequencyPerYear: 4, 20 | timePeriodList: [] 21 | }; 22 | 23 | class MockActivatedRoute extends ActivatedRoute { 24 | data = of({ 25 | child3: UtilitiesService.cloneDeep(child3) 26 | }); 27 | } 28 | 29 | const mockChild3DataService = jasmine.createSpyObj( 30 | 'Child3DataService', ['getChild3']); 31 | 32 | mockChild3DataService.getChild3.and.returnValue( 33 | new Promise((resolve) => { 34 | resolve(child3); 35 | })); 36 | 37 | describe('ChildComponent3Component', () => { 38 | let component: ChildComponent3Component; 39 | beforeAll(() => { 40 | TestInjector.setInjector(); 41 | }); 42 | 43 | beforeEach(() => { 44 | component = new ChildComponent3Component( 45 | new MockActivatedRoute(), 46 | mockChild3DataService); 47 | }); 48 | 49 | it('should be created', async(() => { 50 | component.componentLoadingComplete.subscribe(() => { 51 | expect(component).toBeTruthy(); 52 | }); 53 | component.ngOnInit(); 54 | })); 55 | 56 | it('can toggle edit mode', async(() => { 57 | component.componentLoadingComplete.subscribe(() => { 58 | component.toggleEditMode(true); 59 | expect(component.editMode).toBe(true); 60 | }); 61 | component.ngOnInit(); 62 | })); 63 | 64 | it('can save frequency data ', async(() => { 65 | component.componentLoadingComplete.subscribe(() => { 66 | component.toggleEditMode(true); 67 | component.child3.statementFrequencyPerYear = 4; 68 | component.save().then(() => { 69 | expect(component.child3.statementFrequencyPerYear).toEqual(4); 70 | }); 71 | }); 72 | component.ngOnInit(); 73 | })); 74 | 75 | it('can undo edit mode', async(() => { 76 | component.componentLoadingComplete.subscribe(() => { 77 | component.toggleEditMode(true); 78 | component.child3.processingFrequencyPerYear = 365; 79 | component.toggleEditMode(false); 80 | expect(component.editMode).toBe(false); 81 | }); 82 | component.ngOnInit(); 83 | })); 84 | }); 85 | -------------------------------------------------------------------------------- /src/app/demo/accounts/child-component3/child-component3.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { DemoTransactionComponent } from '../shared/components/demo-transaction-component'; 4 | import { Child3DataService } from '../shared/services/child3-data.service'; 5 | import { IPartsFormControl } from '../../../framework/models/form-controls.models'; 6 | import { IChild3Entity } from '../shared/models/child3-entity.models'; 7 | 8 | @Component({ 9 | selector: 'la-child-component3', 10 | templateUrl: './child-component3.component.html', 11 | styleUrls: ['./child-component3.component.scss'], 12 | encapsulation: ViewEncapsulation.None 13 | }) 14 | 15 | export class ChildComponent3Component extends DemoTransactionComponent { 16 | 17 | child3: IChild3Entity; 18 | private originalChild3: IChild3Entity; 19 | formControls: Array = [ 20 | { name: 'adminYearEndDate' }, 21 | { name: 'statementYearEndDate' }, 22 | { name: 'adminFrequencyPerYear' }, 23 | { name: 'statementFrequencyPerYear' }, 24 | { name: 'processingFrequencyPerYear' }, 25 | { name: 'billingFrequencyPerYear' } 26 | ]; 27 | protected routeParamName = 'child3'; 28 | 29 | constructor(protected route: ActivatedRoute, 30 | private child3DataService: Child3DataService) { 31 | super(route); 32 | } 33 | 34 | protected get original() { 35 | return this.originalChild3; 36 | } 37 | 38 | protected set original(value: IChild3Entity) { 39 | this.originalChild3 = value; 40 | } 41 | 42 | protected get entity() { 43 | return this.child3; 44 | } 45 | 46 | protected set entity(value: IChild3Entity) { 47 | this.child3 = value; 48 | } 49 | 50 | protected get dataService() { 51 | return this.child3DataService; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/components/navigation-error/navigation-error.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/components/navigation-error/navigation-error.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/demo/accounts/shared/components/navigation-error/navigation-error.component.scss -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/components/navigation-error/navigation-error.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | import { NavigationErrorComponent } from './navigation-error.component'; 5 | import { TestInjector } from '../../../../../demo-common/testing/testing-helpers'; 6 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 7 | 8 | class MockActivatedRoute extends ActivatedRoute { 9 | params = of({ 10 | code: 'CF0102' 11 | }); 12 | } 13 | 14 | describe('NavigationErrorComponent', () => { 15 | let component: NavigationErrorComponent; 16 | 17 | beforeAll(() => { 18 | TestInjector.setInjector(); 19 | }); 20 | beforeEach(() => { 21 | component = new NavigationErrorComponent( 22 | new MockActivatedRoute()); 23 | component.ngOnInit(); 24 | }); 25 | 26 | it('should be created', () => { 27 | component.componentLoadingComplete.subscribe(() => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | component.ngOnInit(); 31 | }); 32 | 33 | it('empty error message shows empty', async(() => { 34 | component.componentLoadingComplete.subscribe(() => { 35 | expect(component.header).toEqual(''); 36 | }); 37 | component.ngOnInit(); 38 | })); 39 | 40 | it('can show error message passed in from the route', async(() => { 41 | // Cast types to get around the service not being public 42 | ((component).userSessionService).navigationError = { 43 | message: 'Error', 44 | navigatingTo: '', 45 | serverError: null 46 | }; 47 | component.componentLoadingComplete.subscribe(() => { 48 | expect(component.errorsFromServer).toEqual(['Error']); 49 | }); 50 | component.ngOnInit(); 51 | })); 52 | 53 | it('can show error title when URL included in from the route', async(() => { 54 | // Cast types to get around the service not being public 55 | ((component).userSessionService).navigationError = { 56 | message: 'Error', 57 | navigatingTo: '/accounts', 58 | serverError: null 59 | }; 60 | component.componentLoadingComplete.subscribe(() => { 61 | expect(component.header).toContain('/accounts'); 62 | }); 63 | component.ngOnInit(); 64 | })); 65 | }); 66 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/components/navigation-error/navigation-error.component.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute, Router } from '@angular/router'; 2 | import { Component, OnInit, OnDestroy } from '@angular/core'; 3 | import { DemoTransactionComponent } from '../demo-transaction-component'; 4 | 5 | @Component({ 6 | selector: 'la-navigation-error', 7 | templateUrl: './navigation-error.component.html', 8 | styleUrls: ['./navigation-error.component.scss'] 9 | }) 10 | export class NavigationErrorComponent extends DemoTransactionComponent implements OnInit, OnDestroy { 11 | 12 | header: string; 13 | 14 | constructor(protected route: ActivatedRoute) { 15 | super(route); 16 | } 17 | 18 | ngOnInit() { 19 | this.eventSubscriptions.push(this.route.params 20 | .subscribe(params => { 21 | const accountCode = params['code']; 22 | if (!accountCode || accountCode.toLowerCase() === 'undefined') { 23 | this.router.navigate(['accounts/searchresults']); 24 | } else { 25 | this.errorsFromServer = []; 26 | this.header = ''; 27 | if (this.userSessionService.navigationError) { 28 | this.errorsFromServer = 29 | this.errorUtilitiesService.parseNavigationError(this.userSessionService.navigationError); 30 | if (this.userSessionService.navigationError.navigatingTo) { 31 | this.header = `Unable to navigate to the requested URL: ${this.userSessionService.navigationError.navigatingTo}`; 32 | } 33 | this.userSessionService.navigationError = null; 34 | } 35 | super.ngOnInit(); 36 | } 37 | })); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../../scss/colors'; 2 | @import '../../../../../../scss/mixins'; 3 | 4 | .account-header { 5 | margin: -35px -15px -35px -15px; 6 | padding: 1px 15px; 7 | background: $background; 8 | min-width: 768px; 9 | } 10 | .ui-menu.ui-menubar { 11 | width: 50%; 12 | } 13 | .commitPanel { 14 | position: fixed; 15 | right: 0px; 16 | background: $background; 17 | z-index: 100; 18 | padding: 15px 30px 0px 15px; 19 | margin-right: -10px; 20 | margin-top: -5px; 21 | } 22 | p, ul { 23 | margin-bottom: 0; 24 | } 25 | h1 a { 26 | font-weight: bold; 27 | } 28 | a { 29 | color: #333; 30 | } 31 | a > i { 32 | color: #666; 33 | } 34 | a:hover, a:hover > i { 35 | text-decoration: none; 36 | cursor: pointer; 37 | color: #999 !important; 38 | } 39 | .hamburger-menu { 40 | margin-top:6px; 41 | } 42 | .page-title { 43 | margin-top: 5px; 44 | text-overflow: ellipsis; 45 | @include font-page-title(); 46 | .mega-menu { 47 | padding-right: 10px; 48 | } 49 | a { 50 | font-weight: bold; 51 | i { 52 | font-size: 1em; 53 | } 54 | } 55 | } 56 | p { 57 | @include font-p(); 58 | } 59 | .fixed-top { 60 | position: fixed; 61 | z-index: 100; 62 | width: 100%; 63 | margin-top:-12px; 64 | padding-top:5px; 65 | } 66 | 67 | #pn-submit-btn { 68 | @include submit-button(); 69 | margin-top: 0; 70 | } 71 | 72 | .page-title { 73 | max-width: 992px; 74 | text-overflow: ellipsis; 75 | white-space: nowrap; 76 | overflow: hidden; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 5 | import { TestInjector, MockUserSessionService, MockActivatedRoute } from '../../../../../demo-common/testing/testing-helpers'; 6 | import { AccountHeaderComponent } from './account-header.component'; 7 | import { AccountHeaderService } from './account-header.service'; 8 | import { IAccount } from '../../models/account.models'; 9 | import { TransactionService } from '../../../../../demo-common/services/transaction.service'; 10 | import { GlobalEventsService } from '../../../../../framework/services/global-events.service'; 11 | 12 | const account = { 13 | accountCode: 'ABC123', 14 | accountName: 'Test Account' 15 | }; 16 | 17 | class MockAccountDataService { 18 | currentAccount = { 19 | accountCode: 'ABC123', 20 | accountName: 'Test Account', 21 | accountType: 'Personal', 22 | accountStatus: 'Active' 23 | }; 24 | accountChanged = of(); 25 | userSelectedAnotherAccount(): boolean { 26 | return false; 27 | } 28 | getAccount(): Promise { 29 | return new Promise((resolve) => { 30 | resolve(account); 31 | }); 32 | } 33 | userUpdatedAccountName(name1: string, name2: string, name3: string) { 34 | } 35 | } 36 | 37 | describe('AccountHeaderComponent', () => { 38 | let component: AccountHeaderComponent; 39 | beforeAll(() => { 40 | TestInjector.setInjector(); 41 | }); 42 | 43 | beforeEach(() => { 44 | component = new AccountHeaderComponent( 45 | TestInjector.getService(Router), 46 | new MockActivatedRoute(), 47 | new AccountHeaderService(new MockAccountDataService(), new MockUserSessionService()), 48 | TestInjector.getService(TransactionService), 49 | TestInjector.getService(UserSessionService), 50 | TestInjector.getService(GlobalEventsService)); 51 | }); 52 | 53 | it('should be created', async(() => { 54 | component.ngOnInit(); 55 | expect(component).toBeTruthy(); 56 | })); 57 | 58 | it('can show account name in header', async(() => { 59 | component.ngOnInit(); 60 | expect(component.topHeader).toContain('Test Account'); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Router, ActivatedRoute } from '@angular/router'; 2 | import { Component, OnInit, EventEmitter, Output } from '@angular/core'; 3 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 4 | import { TransactionService } from '../../../../../demo-common/services/transaction.service'; 5 | import { AccountHeaderService } from './account-header.service'; 6 | import { BaseComponent } from '../../../../../framework/components/base-component'; 7 | import { IServerError } from '../../../../../framework/validation/models/server-error.models'; 8 | import { GlobalEventsService } from '../../../../../framework/services/global-events.service'; 9 | 10 | @Component({ 11 | selector: 'la-account-header', 12 | templateUrl: './account-header.component.html', 13 | styleUrls: ['./account-header.component.scss'] 14 | }) 15 | export class AccountHeaderComponent extends BaseComponent implements OnInit { 16 | 17 | @Output() postChanges: EventEmitter = new EventEmitter(); 18 | errorsFromServer = []; 19 | isAccountReadOnly = false; 20 | 21 | constructor(private router: Router, 22 | private route: ActivatedRoute, 23 | public accountHeaderService: AccountHeaderService, 24 | private transactionService: TransactionService, 25 | private userSessionService: UserSessionService, 26 | protected globalEventsService: GlobalEventsService) { 27 | super(); 28 | } 29 | 30 | ngOnInit() { 31 | if (this.route.params) { 32 | this.eventSubscriptions.push(this.route.params.subscribe((params) => { 33 | const accountCode = params['code']; 34 | this.userSessionService.accountCode = accountCode; 35 | 36 | this.accountHeaderService.initialize() 37 | .catch((error: IServerError) => { 38 | this.errorsFromServer = this.populateErrors(error); 39 | }); 40 | })); 41 | } 42 | } 43 | 44 | get accountCode() { 45 | return this.accountHeaderService.accountCode; 46 | } 47 | 48 | get topHeader() { 49 | let header = ''; 50 | if (this.accountHeaderService.accountCode) { 51 | header += this.accountHeaderService.accountCode.toUpperCase(); 52 | } 53 | 54 | if (this.accountHeaderService.accountName) { 55 | header += ' - ' + this.accountHeaderService.accountName; 56 | } 57 | return header; 58 | } 59 | 60 | refresh() { 61 | return this.accountHeaderService.refresh() 62 | .catch((error: IServerError) => { 63 | this.errorsFromServer = this.populateErrors(error); 64 | }); 65 | } 66 | 67 | postChangesClick() { 68 | this.postChanges.emit(); 69 | } 70 | 71 | canCommit() { 72 | return this.transactionService.canCommit(); 73 | } 74 | 75 | goToAccountHome() { 76 | this.router.navigate([`${this.accountRouteRoot}`]); 77 | } 78 | 79 | private get accountRouteRoot() { 80 | return `/accounts/${this.userSessionService.accountCode}`; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject, async } from '@angular/core/testing'; 2 | import { of } from 'rxjs'; 3 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 4 | import { IAccount } from '../../models/account.models'; 5 | import { AccountDataService } from '../../services/account-data.service'; 6 | import { AccountHeaderService } from './account-header.service'; 7 | import { UtilitiesService } from '../../../../../framework/services/utilities.service'; 8 | 9 | describe('AccountHeaderService', () => { 10 | 11 | const account = { 12 | accountName: 'Test Account' 13 | }; 14 | let service: AccountHeaderService; 15 | 16 | class MockAccountDataService { 17 | currentAccount = { 18 | accountCode: 'ABC123', 19 | accountName: 'Test Account', 20 | accountType: 'Personal', 21 | accountStatus: 'Active' 22 | }; 23 | accountChanged = of(); 24 | userSelectedAnotherAccount(): boolean { 25 | return false; 26 | } 27 | getAccount(): Promise { 28 | return new Promise((resolve) => { 29 | resolve(account); 30 | }); 31 | } 32 | userUpdatedAccountName(name: string) { 33 | } 34 | } 35 | 36 | beforeEach(() => { 37 | TestBed.configureTestingModule({ 38 | providers: [ 39 | AccountHeaderService, 40 | UserSessionService, 41 | UtilitiesService, 42 | { 43 | provide: AccountDataService, 44 | useClass: MockAccountDataService 45 | } 46 | ] 47 | }); 48 | }); 49 | 50 | beforeEach(inject([AccountHeaderService], (s: AccountHeaderService) => { 51 | service = s; 52 | })); 53 | 54 | it('should create the service', () => { 55 | expect(service).toBeTruthy(); 56 | }); 57 | 58 | it('can initialize', async () => { 59 | const currentAccount = await service.initialize(); 60 | expect(currentAccount).toBeDefined(); 61 | }); 62 | 63 | it('can initialize with same account selected', async(() => { 64 | service.initialize().then((currentAccount) => { 65 | expect(currentAccount.accountName).toBe(account.accountName); 66 | }).catch(error => { 67 | expect(error).toBeNull(); 68 | }); 69 | })); 70 | 71 | it('can update account name', async(() => { 72 | service.initialize().then((currentAccount) => { 73 | service.updateAccountName('new name'); 74 | expect(service.accountName).toBe('new name'); 75 | }).catch(error => { 76 | expect(error).toBeNull(); 77 | }); 78 | })); 79 | }); 80 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/account-header/account-header.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs'; 3 | import { AccountDataService } from '../../services/account-data.service'; 4 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 5 | 6 | @Injectable() 7 | export class AccountHeaderService { 8 | accountCode: string; 9 | accountName: string; 10 | accountType: string; 11 | accountStatus: string; 12 | private accountChangedSubscription: Subscription; 13 | private accountChangedSource = new Subject(); 14 | accountChanged = this.accountChangedSource.asObservable(); 15 | 16 | constructor(private accountDataService: AccountDataService, 17 | private userSessionService: UserSessionService) { 18 | } 19 | 20 | initialize() { 21 | // After getting account data from the server, 22 | // update the header to match and let anyone using the header 23 | // that the data has changed, so the menu can be updated 24 | 25 | if (this.accountDataService.currentAccount && 26 | !this.accountDataService.userSelectedAnotherAccount()) { 27 | this.populateFromDataService(); 28 | this.handleAccountChanged(); 29 | return Promise.resolve(this.accountDataService.currentAccount); 30 | } else { 31 | return this.refresh(); 32 | } 33 | } 34 | 35 | refresh() { 36 | const promise = new Promise((resolve, reject) => { 37 | this.accountDataService.getAccount() 38 | .then(() => { 39 | this.populateFromDataService(); 40 | this.handleAccountChanged(); 41 | resolve(); 42 | }).catch((error) => { 43 | this.clearAccountHeader(); 44 | reject(error); 45 | }); 46 | }); 47 | return promise; 48 | } 49 | updateAccountName(name: string) { 50 | this.accountName = name; 51 | this.accountDataService.userUpdatedAccountName(name); 52 | } 53 | 54 | private handleAccountChanged() { 55 | if (this.accountDataService.accountChanged && !this.accountChangedSubscription) { 56 | this.accountChangedSubscription = this.accountDataService.accountChanged.subscribe(() => { 57 | this.populateFromDataService(); 58 | this.accountChangedSource.next(); 59 | }); 60 | } 61 | } 62 | 63 | private clearAccountHeader() { 64 | this.accountCode = ''; 65 | this.accountName = ''; 66 | this.accountType = ''; 67 | this.accountStatus = ''; 68 | this.userSessionService.clearAccount(); 69 | } 70 | 71 | private populateFromDataService() { 72 | this.accountCode = this.accountDataService.currentAccount.accountCode.toUpperCase(); 73 | this.accountName = this.accountDataService.currentAccount.accountName; 74 | this.accountType = this.accountDataService.currentAccount.accountType; 75 | this.accountStatus = this.accountDataService.currentAccount.accountStatus; 76 | this.userSessionService.accountType = this.accountDataService.currentAccount.accountType; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/header-menu/header-menu.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/directives/header-menu/header-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Router, ActivatedRoute } from '@angular/router'; 2 | import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'; 3 | import { Subject } from 'rxjs'; 4 | import { MenuItem} from 'primeng/primeng'; 5 | import { UserSessionService } from '../../../../../demo-common/services/user-session.service'; 6 | import { AccountHeaderService } from '../account-header/account-header.service'; 7 | import { MenuService } from '../../services/menu.service'; 8 | import { BaseComponent } from '../../../../../framework/components/base-component'; 9 | import { IServerError } from '../../../../../framework/validation/models/server-error.models'; 10 | import { GlobalEventsService } from '../../../../../framework/services/global-events.service'; 11 | 12 | @Component({ 13 | selector: 'la-header-menu', 14 | templateUrl: './header-menu.component.html', 15 | styleUrls: ['./header-menu.component.scss'], 16 | encapsulation: ViewEncapsulation.None 17 | }) 18 | 19 | export class HeaderMenuComponent extends BaseComponent implements OnInit, OnDestroy { 20 | 21 | items: MenuItem[]; 22 | errorsFromServer = []; 23 | private menuUpdatedSource = new Subject(); 24 | menuUpdated = this.menuUpdatedSource.asObservable(); 25 | 26 | constructor(private router: Router, 27 | private route: ActivatedRoute, 28 | public accountHeaderService: AccountHeaderService, 29 | private userSessionService: UserSessionService, 30 | private menuService: MenuService, 31 | protected globalEventsService: GlobalEventsService) { 32 | super(); 33 | this.buildMenu(); 34 | } 35 | 36 | ngOnInit() { 37 | if (this.route.params) { 38 | this.route.params.subscribe((params) => { 39 | const accountCode = params['code']; 40 | if (accountCode) { 41 | this.userSessionService.accountCode = accountCode; 42 | 43 | this.accountHeaderService.initialize().then(() => { 44 | this.buildMenu(); 45 | this.eventSubscriptions.push(this.accountHeaderService.accountChanged.subscribe(() => { 46 | this.buildMenu(); 47 | })); 48 | }).catch((error: IServerError) => { 49 | this.items = null; 50 | this.errorsFromServer = this.populateErrors(error); 51 | }); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | navigate(event, path) { 58 | this.router.navigate(path, { relativeTo: this.route }); 59 | } 60 | 61 | private buildMenu() { 62 | this.menuService.buildMenu(this.accountHeaderService.accountCode); 63 | this.items = this.menuService.menuItems; 64 | this.menuUpdatedSource.next(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/models/account.models.ts: -------------------------------------------------------------------------------- 1 | import { IEntity } from '../../../../demo-common/models/transaction.models'; 2 | 3 | export interface IAccount extends IEntity { 4 | accountCode: string; 5 | accountName: string; 6 | accountType: string; 7 | accountStatus: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/models/child1-entity.models.ts: -------------------------------------------------------------------------------- 1 | import { IPartsListChoice } from '../../../../framework/models/form-controls.models'; 2 | import { IEntity } from '../../../../demo-common/models/transaction.models'; 3 | 4 | export interface IChild1Entity extends IEntity { 5 | accountCode: string; 6 | name1: string; 7 | name2: string; 8 | name3: string; 9 | addressLine1: string; 10 | addressLine2: string; 11 | addressLine3: string; 12 | city: string; 13 | zip: string; 14 | state: string; 15 | product: string; 16 | accountType: string; 17 | accountStatus: string; 18 | startDate: Date; 19 | lastModifiedDate: Date; 20 | statusCodeList: Array; 21 | productCodeList: Array; 22 | stateCodeList: Array; 23 | accountTypeList: Array; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/models/child2-entity.models.ts: -------------------------------------------------------------------------------- 1 | import { IPartsListChoice } from '../../../../framework/models/form-controls.models'; 2 | import { IEntity } from '../../../../demo-common/models/transaction.models'; 3 | 4 | export interface IChild2Entity extends IEntity { 5 | accountType: string; 6 | contactName: string; 7 | contactPhoneAreaCode: string; 8 | contactPhoneNumber: string; 9 | name1: string; 10 | name2: string; 11 | name3: string; 12 | rate: number; 13 | accountTypeList: Array; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/models/child3-entity.models.ts: -------------------------------------------------------------------------------- 1 | import { IPartsListChoice } from '../../../../framework/models/form-controls.models'; 2 | import { IEntity } from '../../../../demo-common/models/transaction.models'; 3 | 4 | export interface IChild3Entity extends IEntity { 5 | accountCode: string; 6 | adminYearEndDate: Date; 7 | adminFrequencyPerYear: number; 8 | statementYearEndDate: Date; 9 | statementFrequencyPerYear: number; 10 | processingFrequencyPerYear: number; 11 | billingFrequencyPerYear: number; 12 | timePeriodList: Array; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/account-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { TransactionEntityDataService } from '../../../../demo-common/services/transaction-entity-data.service'; 4 | import { IAccount } from '../models/account.models'; 5 | 6 | @Injectable() 7 | export class AccountDataService extends TransactionEntityDataService { 8 | 9 | private _currentAccount: IAccount; 10 | private accountChangedSource = new Subject(); 11 | accountChanged = this.accountChangedSource.asObservable(); 12 | 13 | get currentAccount() { 14 | return this._currentAccount; 15 | } 16 | 17 | userSelectedAnotherAccount() { 18 | return this.currentAccount.accountCode !== this.userSessionService.accountCode; 19 | } 20 | 21 | userUpdatedAccountName(name) { 22 | this.currentAccount.accountName = name; 23 | } 24 | 25 | getAccount(): Promise { 26 | const endpoint = this.endpoint(); 27 | return this.get(endpoint).then((account: IAccount) => { 28 | this._currentAccount = account; 29 | this.accountChangedSource.next(); 30 | return account; 31 | }); 32 | } 33 | 34 | endpoint() { 35 | return this.constructEndpoint(''); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/child1-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TransactionEntityDataService } from '../../../../demo-common/services/transaction-entity-data.service'; 3 | import { IChild1Entity } from '../models/child1-entity.models'; 4 | 5 | @Injectable() 6 | export class Child1DataService extends TransactionEntityDataService { 7 | 8 | getChild1(): Promise { 9 | const endpoint = this.endpointWithTransactionId(); 10 | return this.get(endpoint); 11 | } 12 | 13 | endpoint() { 14 | return this.constructEndpoint('class1'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/child2-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TransactionEntityDataService } from '../../../../demo-common/services/transaction-entity-data.service'; 3 | import { IChild2Entity } from '../models/child2-entity.models'; 4 | 5 | @Injectable() 6 | export class Child2DataService extends TransactionEntityDataService { 7 | 8 | getChild2(): Promise { 9 | const endpoint = this.endpointWithTransactionId(); 10 | return this.get(endpoint); 11 | } 12 | 13 | endpoint() { 14 | return this.constructEndpoint('class2'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/child3-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TransactionEntityDataService } from '../../../../demo-common/services/transaction-entity-data.service'; 3 | import { IChild3Entity } from '../models/child3-entity.models'; 4 | 5 | @Injectable() 6 | export class Child3DataService extends TransactionEntityDataService { 7 | 8 | getChild3(): Promise { 9 | const endpoint = this.endpointWithTransactionId(); 10 | return this.get(endpoint); 11 | } 12 | 13 | endpoint() { 14 | return this.constructEndpoint('class3'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/demo-account-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot } from '@angular/router'; 3 | import { DemoResolver } from '../../../../demo-common/services/demo-resolver.service'; 4 | 5 | @Injectable() 6 | export class DemoAccountResolver extends DemoResolver { 7 | 8 | resolve(route: ActivatedRouteSnapshot) { 9 | const accountCode = route.params['code']; 10 | this.userSessionService.accountCode = accountCode; 11 | return super.resolve(route); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/demo/accounts/shared/services/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MenuItem } from 'primeng/primeng'; 3 | import { AuthorizationService } from '../../../../framework/services/authorization.service'; 4 | import { ActionCode } from '../../../../framework/models/authorization.types'; 5 | 6 | @Injectable() 7 | export class MenuService { 8 | 9 | menuItems: MenuItem[]; 10 | private id: string; 11 | 12 | constructor(private authorizationService: AuthorizationService) { 13 | } 14 | 15 | buildMenu(id: string) { 16 | this.id = id ? id.toLowerCase() : ''; 17 | const accountRouteRoot = `/accounts/${this.id}`; 18 | this.menuItems = [ 19 | { 20 | label: 'Component #1', 21 | routerLink: [accountRouteRoot], 22 | visible: this.showMenuItem('VIEW') 23 | }, 24 | { 25 | label: 'Component #2', 26 | routerLink: [accountRouteRoot + `/child2`], 27 | visible: this.showMenuItem('VIEW') 28 | }, 29 | { 30 | label: 'Component #3', 31 | routerLink: [accountRouteRoot + '/child3'], 32 | visible: this.showMenuItem('VIEW') 33 | } 34 | ] 35 | } 36 | 37 | private showMenuItem(actionCode: ActionCode) { 38 | return this.authorizationService.hasPermission(actionCode); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/demo/demo.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { DemoCommonModule } from '../demo-common/demo-common.module'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | import { DemoRoutingModule } from './demo-routing.module'; 7 | import { UpdateService } from './accounts/shared/services/update.service'; 8 | import { AccountDataService } from './accounts/shared/services/account-data.service'; 9 | import { AccountHeaderService } from './accounts/shared/directives/account-header/account-header.service'; 10 | import { MenuService } from './accounts/shared/services/menu.service'; 11 | import { DemoTransactionComponent } from './accounts/shared/components/demo-transaction-component'; 12 | import { AccountHeaderComponent } from './accounts/shared/directives/account-header/account-header.component'; 13 | import { ChildComponent1Resolver } from './accounts/child-component1/child-component1-resolver.service'; 14 | import { SearchResultsComponent } from './search/search-results/search-results.component'; 15 | import { SearchResultsResolver } from './search/search-results/search-results-resolver.service'; 16 | import { Child3DataService } from './accounts/shared/services/child3-data.service'; 17 | import { HeaderMenuComponent } from './accounts/shared/directives/header-menu/header-menu.component'; 18 | import { Child2DataService } from './accounts/shared/services/child2-data.service'; 19 | import { NavigationErrorComponent } from './accounts/shared/components/navigation-error/navigation-error.component'; 20 | import { ChildComponent1Component } from './accounts/child-component1/child-component1.component'; 21 | import { Child1DataService } from './accounts/shared/services/child1-data.service'; 22 | import { ChildComponent2Component } from './accounts/child-component2/child-component2.component'; 23 | import { ChildComponent2Resolver } from './accounts/child-component2/child-component2-resolver.service'; 24 | import { ChildComponent3Component } from './accounts/child-component3/child-component3.component'; 25 | import { ChildComponent3Resolver } from './accounts/child-component3/child-component3-resolver.service'; 26 | 27 | @NgModule({ 28 | imports: [ 29 | CommonModule, 30 | HttpClientModule, 31 | SharedModule, 32 | DemoCommonModule, 33 | DemoRoutingModule 34 | ], 35 | declarations: [ 36 | DemoTransactionComponent, 37 | ChildComponent1Component, 38 | ChildComponent2Component, 39 | ChildComponent3Component, 40 | AccountHeaderComponent, 41 | SearchResultsComponent, 42 | HeaderMenuComponent, 43 | NavigationErrorComponent 44 | ], 45 | providers: [ 46 | AccountDataService, 47 | UpdateService, 48 | AccountHeaderService, 49 | ChildComponent1Resolver, 50 | ChildComponent2Resolver, 51 | ChildComponent3Resolver, 52 | SearchResultsResolver, 53 | Child1DataService, 54 | Child2DataService, 55 | Child3DataService, 56 | MenuService 57 | ], 58 | exports: [ 59 | ] 60 | }) 61 | export class DemoModule { } 62 | -------------------------------------------------------------------------------- /src/app/demo/search/search-results/search-results-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { IAccountSearchResult } from '../../../demo-common/models/account-search-result.models'; 4 | import { DemoResolver } from '../../../demo-common/services/demo-resolver.service'; 5 | import { SearchService } from '../../../demo-common/services/search.service'; 6 | 7 | @Injectable() 8 | export class SearchResultsResolver extends DemoResolver 9 | implements Resolve> { 10 | 11 | constructor(private searchService: SearchService) { 12 | super(); 13 | } 14 | 15 | resolve(route: ActivatedRouteSnapshot) { 16 | super.resolve(route); 17 | return this.searchService.getAccounts(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/demo/search/search-results/search-results.component.html: -------------------------------------------------------------------------------- 1 |

Account Selection

2 |
3 |
4 | 5 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/demo/search/search-results/search-results.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | @include white-background(); 4 | 5 | .white-bg { 6 | padding-top: 1px !important; 7 | } 8 | .include-inactive { 9 | position: relative; 10 | float: left; 11 | } 12 | .include-inactive { 13 | position: relative; 14 | top: .5em; 15 | } -------------------------------------------------------------------------------- /src/app/demo/search/search-results/search-results.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute, Router } from '@angular/router'; 2 | import { SearchService } from '../../../demo-common/services/search.service'; 3 | import { UserSessionService } from '../../../demo-common/services/user-session.service'; 4 | import { SearchResultsComponent } from './search-results.component'; 5 | import { of } from 'rxjs'; 6 | import { IAccountSearchResult } from '../../../demo-common/models/account-search-result.models'; 7 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 8 | 9 | const searchResults = [{ 10 | accountCode: 'ABC123', 11 | accountName: 'Test Account', 12 | accountType: '' 13 | }, 14 | { 15 | accountCode: 'ABC234', 16 | accountName: 'Test Account2', 17 | accountType: '' 18 | } 19 | ]; 20 | 21 | class MockActivatedRoute extends ActivatedRoute { 22 | data = of({ searchResults: searchResults }); 23 | } 24 | 25 | class MockSearchService { 26 | getAccounts(): Promise> { 27 | return new Promise>((resolve) => { 28 | resolve(>searchResults); 29 | }); 30 | } 31 | } 32 | 33 | describe('SearchResultsComponent', () => { 34 | let component: SearchResultsComponent; 35 | 36 | beforeAll(() => { 37 | TestInjector.setInjector([ 38 | { provide: SearchService, useClass: MockSearchService } 39 | ]); 40 | }); 41 | beforeEach(() => { 42 | component = new SearchResultsComponent( 43 | new MockActivatedRoute(), 44 | TestInjector.getService(UserSessionService)); 45 | component.ngOnInit(); 46 | }); 47 | 48 | it('should be created', () => { 49 | expect(component).toBeTruthy(); 50 | }); 51 | 52 | it('should get all accounts by default', () => { 53 | expect(component.searchResultList.length).toBe(2); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/demo/search/search-results/search-results.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { BaseComponent } from '../../../framework/components/base-component'; 4 | import { IDataTableColumn } from '../../../shared/directives/data-table/data-table-models'; 5 | import { IAccountSearchResult } from '../../../demo-common/models/account-search-result.models'; 6 | import { UserSessionService } from '../../../demo-common/services/user-session.service'; 7 | 8 | @Component({ 9 | selector: 'la-search-results', 10 | templateUrl: './search-results.component.html', 11 | styleUrls: ['./search-results.component.scss'] 12 | }) 13 | export class SearchResultsComponent extends BaseComponent implements OnInit, OnDestroy { 14 | 15 | searchResultList: Array = []; 16 | 17 | searchResultColumns: Array = [ 18 | { name: 'accountCode', header: 'Account Code', sortable: true, dataType: 'url', width: '9%', 19 | link: 'accounts/:accountCode' }, 20 | { name: 'accountName', header: 'Account Name', sortable: true, dataType: 'url', width: '33%', 21 | link: 'accounts/:accountCode' }, 22 | { name: 'accountType', header: 'Account Type', sortable: true, width: '9%' }, 23 | { name: 'accountStatusDisplay', header: 'Status', sortable: true, width: '8%', excludeFromGlobalFilter: true } 24 | ]; 25 | 26 | errorsFromServer: Array = []; 27 | 28 | constructor(private route: ActivatedRoute, 29 | private userSessionService: UserSessionService) { 30 | super(); 31 | } 32 | 33 | ngOnInit() { 34 | super.ngOnInit(); 35 | 36 | this.eventSubscriptions.push(this.route.data 37 | .subscribe((data: { searchResults: Array }) => { 38 | this.errorsFromServer = []; 39 | if (this.userSessionService.navigationError) { 40 | this.errorsFromServer = 41 | this.errorUtilitiesService.parseNavigationError(this.userSessionService.navigationError); 42 | this.userSessionService.navigationError = null; 43 | } 44 | if (data.searchResults) { 45 | this.searchResultList = data.searchResults; 46 | } else { 47 | this.searchResultList = []; 48 | } 49 | })); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/framework/components/base-component.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, FormBuilder } from '@angular/forms'; 2 | import { BaseComponent } from './base-component'; 3 | import { IServerError } from '../validation/models/server-error.models'; 4 | import { TestInjector } from '../../demo-common/testing/testing-helpers'; 5 | 6 | const serverError = { 7 | details: [ 8 | { 9 | code: 1, 10 | message: 'Field Error', 11 | target: 'fieldName' 12 | } 13 | ], 14 | code: 1, 15 | message: 'Test Error', 16 | target: 'field' 17 | }; 18 | 19 | class TestBaseComponent extends BaseComponent { 20 | populateErrors(error: IServerError, form?: FormGroup) { 21 | return super.populateErrors(error, form); 22 | } 23 | } 24 | 25 | describe('BaseComponent', () => { 26 | let component: TestBaseComponent; 27 | beforeAll(() => { 28 | TestInjector.setInjector(); 29 | }); 30 | 31 | beforeEach(() => { 32 | component = new TestBaseComponent(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | 39 | it('can populate error with no form', () => { 40 | const errorList = component.populateErrors(serverError); 41 | expect(errorList[0]).toBe('Test Error'); 42 | expect(errorList[1]).toBe('Field Error'); 43 | }); 44 | 45 | it('can associate a field-level error with a field on a form', () => { 46 | const formBuilder = new FormBuilder(); 47 | const form = formBuilder.group({ 48 | fieldName: ['', []] 49 | }); 50 | const errorList = component.populateErrors(serverError, form); 51 | const control = form.get('fieldName'); 52 | expect(control.errors['serverValidationError']).toBe('Field Error'); 53 | }); 54 | 55 | it('can assign error with missing field as a model error', () => { 56 | const formBuilder = new FormBuilder(); 57 | const form = formBuilder.group({ 58 | fieldName2: ['', []] 59 | }); 60 | const errorList = component.populateErrors(serverError, form); 61 | expect(errorList[0]).toBe('Test Error'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/app/framework/errorhandling/error-handler.service.spec.ts: -------------------------------------------------------------------------------- 1 | ; 2 | import { TestBed, inject } from '@angular/core/testing'; 3 | import { MockLoggingService } from '../../demo-common/testing/testing-helpers'; 4 | import { LoggingService } from '../logging/logging.service'; 5 | import { ErrorHandlerService } from './error-handler.service'; 6 | 7 | const error: Error = { 8 | name: 'Test Error', 9 | message: 'An error occurred', 10 | stack: null 11 | }; 12 | 13 | const error2 = { 14 | name: 'Test Error', 15 | message: 'An error occurred', 16 | stack: null, 17 | originalError: { 18 | name: 'Original Error', 19 | message: 'Parent error occurred', 20 | stack: null 21 | } 22 | }; 23 | 24 | describe('PartsErrorHandlerService', () => { 25 | beforeEach(() => { 26 | 27 | TestBed.configureTestingModule({ 28 | providers: [ 29 | { 30 | provide: LoggingService, 31 | useClass: MockLoggingService 32 | }, 33 | ErrorHandlerService 34 | ] 35 | }); 36 | }); 37 | 38 | it('should create the service', inject([ErrorHandlerService], (service: ErrorHandlerService) => { 39 | expect(service).toBeTruthy(); 40 | })); 41 | 42 | it('should log exception when handling an error', 43 | inject([ErrorHandlerService, LoggingService], 44 | (service: ErrorHandlerService, loggingService: LoggingService) => { 45 | spyOn(loggingService, 'logException'); 46 | service.handleError(error); 47 | expect(loggingService.logException).toHaveBeenCalled(); 48 | })); 49 | 50 | it('should log exception when handling an error with embedded error', 51 | inject([ErrorHandlerService, LoggingService], 52 | (service: ErrorHandlerService, loggingService: LoggingService) => { 53 | spyOn(loggingService, 'logException'); 54 | service.handleError(error2); 55 | expect(loggingService.logException).toHaveBeenCalledTimes(2); 56 | })); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app/framework/errorhandling/error-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable } from '@angular/core'; 2 | import { LoggingService } from '../logging/logging.service'; 3 | 4 | @Injectable() 5 | export class ErrorHandlerService extends ErrorHandler { 6 | 7 | constructor(private loggingService: LoggingService) { 8 | super(); 9 | } 10 | 11 | handleError(error: Error) { 12 | this.loggingService.logException(error); // Manually log exception 13 | const originalError = this.getOriginalError(error); 14 | if (originalError !== error) { 15 | this.loggingService.logException(originalError); // Manually log original exception 16 | } 17 | } 18 | 19 | private getOriginalError(error: any) { 20 | while (error && error.originalError) { 21 | error = error.originalError; 22 | } 23 | return (error); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/framework/errorhandling/error-utilities.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IServerError, INavigationError } from '../validation/models/server-error.models'; 3 | 4 | @Injectable() 5 | export class ErrorUtilitiesService { 6 | 7 | parseFieldErrors(error: IServerError) { 8 | const parsedErrors: Array<{ fieldName: string, errorMessage: string }> = []; 9 | if (error && error.details && error.details.length > 0) { 10 | error.details.forEach(detail => { 11 | const fieldName = detail.target; 12 | const errorMessage = detail.message ? detail.message : error.message; 13 | parsedErrors.push({ fieldName: fieldName, errorMessage: errorMessage }); 14 | }); 15 | } 16 | return parsedErrors; 17 | } 18 | 19 | parseModelErrors(error: IServerError | string) { 20 | if (error && (error).message) { 21 | return [(error).message]; 22 | } 23 | return [error]; 24 | } 25 | 26 | parseNavigationError(error: INavigationError) { 27 | let errorsFromServer = []; 28 | if (error) { 29 | if (error.message) { 30 | errorsFromServer = this.parseServerError(error.message); 31 | } else if (error.serverError) { 32 | errorsFromServer = this.parseServerError(error.serverError); 33 | } 34 | } 35 | return errorsFromServer; 36 | } 37 | 38 | parseServerError(error: IServerError | string) { 39 | let errorsFromServer: Array = []; 40 | if (typeof error === 'string') { 41 | errorsFromServer = [error]; 42 | } else if (error) { 43 | if (error.details && error.details.length > 0) { 44 | error.details.forEach(detail => { 45 | const errorMessage = detail.message ? detail.message : error.message; 46 | errorsFromServer.push(errorMessage); 47 | }); 48 | } 49 | if (error.message) { 50 | if (!errorsFromServer.find(detailError => { 51 | return detailError === error.message; 52 | })) { 53 | errorsFromServer.unshift(error.message); 54 | } 55 | } 56 | } 57 | 58 | return errorsFromServer; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/app/framework/framework.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { AuthGuardService } from './services/auth-guard.service'; 6 | import { UtilitiesService } from './services/utilities.service'; 7 | import { GlobalEventsService } from './services/global-events.service'; 8 | import { CanDeactivateGuardService } from './services/can-deactivate-guard.service'; 9 | import { FormBuilderService } from './services/form-builder.service'; 10 | import { ErrorHandlerService } from './errorhandling/error-handler.service'; 11 | import { ErrorUtilitiesService } from './errorhandling/error-utilities.service'; 12 | import { MetadataDataService } from './services/metadata-data.service'; 13 | import { MetadataService } from './services/metadata.service'; 14 | import { LoggingService } from './logging/logging.service'; 15 | import { AuthorizationService } from './services/authorization.service'; 16 | import { AuthorizationDataService } from './services/authorization-data.service'; 17 | import { SystemMessageService } from './services/system-message.service'; 18 | import { SystemMessageDataService } from './services/system-message-data.service'; 19 | import { BaseComponent } from './components/base-component'; 20 | import { ValidationModule } from './validation/validation.module'; 21 | import { AdalService } from 'adal-angular4'; 22 | 23 | @NgModule({ 24 | imports: [ 25 | CommonModule, 26 | FormsModule, 27 | RouterModule, 28 | ReactiveFormsModule, 29 | ValidationModule 30 | ], 31 | declarations: [ 32 | BaseComponent 33 | ], 34 | providers: [ 35 | GlobalEventsService, 36 | AdalService, 37 | AuthGuardService, 38 | CanDeactivateGuardService, 39 | FormBuilderService, 40 | ErrorHandlerService, 41 | AuthorizationService, 42 | AuthorizationDataService, 43 | UtilitiesService, 44 | ErrorUtilitiesService, 45 | LoggingService, 46 | MetadataDataService, 47 | MetadataService, 48 | SystemMessageService, 49 | SystemMessageDataService, 50 | { provide: ErrorHandler, useClass: ErrorHandlerService } 51 | ], 52 | exports: [ 53 | CommonModule, 54 | FormsModule, 55 | ReactiveFormsModule 56 | ] 57 | }) 58 | export class FrameworkModule { 59 | } 60 | -------------------------------------------------------------------------------- /src/app/framework/logging/severity-level.model.ts: -------------------------------------------------------------------------------- 1 | export enum SeverityLevel { 2 | Verbose = 0, 3 | Information = 1, 4 | Warning = 2, 5 | Error = 3, 6 | Critical = 4, 7 | } 8 | -------------------------------------------------------------------------------- /src/app/framework/models/authorization.types.ts: -------------------------------------------------------------------------------- 1 | export type ActionCode = 2 | 'VIEW' | 3 | 'UPDATE'; 4 | -------------------------------------------------------------------------------- /src/app/framework/models/form-controls.models.ts: -------------------------------------------------------------------------------- 1 | import { ActionCode } from './authorization.types'; 2 | export interface IPartsFormControl { 3 | name: string; 4 | controls?: Array>; 5 | defaultValue?: any; 6 | disableIfNotAuthorized?: ActionCode; 7 | } 8 | 9 | export interface IPartsListChoice { 10 | value: string | number; 11 | label: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/framework/models/metadata.models.ts: -------------------------------------------------------------------------------- 1 | export interface ITimePeriod { 2 | period: string; 3 | frequency: string; 4 | freqPerYear: number; 5 | uniqueId: number; 6 | } 7 | 8 | export interface ILookupList { 9 | [K: string]: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/framework/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | import { AuthorizationService } from './authorization.service'; 5 | import { AppConfig } from '../../app.config'; 6 | import { ActionCode } from '../models/authorization.types'; 7 | 8 | @Injectable() 9 | export class AuthGuardService implements CanActivate { 10 | 11 | constructor(protected router: Router, protected authService: AuthService, 12 | protected authorizationService: AuthorizationService) { 13 | } 14 | 15 | canActivate(route: ActivatedRouteSnapshot): Promise | boolean { 16 | return this.hasRequiredPermission(route.data['actionCode']); 17 | } 18 | 19 | protected hasRequiredPermission(actionCode: ActionCode): Promise | boolean { 20 | if (!AppConfig.settings.aad.requireAuth || this.authService.isUserAuthenticated) { 21 | if (this.authorizationService.permissions) { 22 | if (actionCode) { 23 | return this.authorizationService.hasPermission(actionCode); 24 | } else { 25 | return this.authorizationService.hasPermission(null); 26 | } 27 | } else { 28 | const promise = new Promise((resolve, reject) => { 29 | this.authorizationService.initializePermissions() 30 | .then(() => { 31 | if (actionCode) { 32 | resolve(this.authorizationService.hasPermission(actionCode)); 33 | } else { 34 | resolve(this.authorizationService.hasPermission(null)); 35 | } 36 | }).catch(() => { 37 | resolve(false); 38 | }); 39 | }); 40 | return promise; 41 | } 42 | } else { 43 | this.authService.login(); 44 | return false; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/framework/services/auth-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler } from '@angular/common/http'; 3 | import { AdalService, AdalInterceptor } from 'adal-angular4'; 4 | import { AppConfig } from '../../app.config'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthInterceptorService extends AdalInterceptor { 10 | _isUserAuthenticated: boolean; 11 | _userName: string; 12 | 13 | constructor(adalService: AdalService) { 14 | super(adalService) 15 | } 16 | 17 | intercept(request: HttpRequest, next: HttpHandler) { 18 | if (request.url.startsWith('assets/config') || !AppConfig.settings.aad.requireAuth) { 19 | return next.handle(request); 20 | } 21 | return super.intercept(request, next); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/framework/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { AuthService } from './auth.service'; 3 | import { LoggingService } from '../logging/logging.service'; 4 | import { MockLoggingService } from '../../demo-common/testing/testing-helpers'; 5 | import { AdalService } from 'adal-angular4'; 6 | 7 | class TestAuthService extends AuthService { 8 | authenticate() {} 9 | getAccessToken() { 10 | return Promise.resolve(''); 11 | } 12 | } 13 | 14 | describe('AuthService', () => { 15 | beforeEach(() => { 16 | 17 | TestBed.configureTestingModule({ 18 | providers: [ 19 | AdalService, 20 | TestAuthService, 21 | { 22 | provide: LoggingService, 23 | useClass: MockLoggingService 24 | } 25 | ] 26 | }); 27 | }); 28 | 29 | it('should create the service', inject([TestAuthService], (service: AuthService) => { 30 | expect(service).toBeTruthy(); 31 | })); 32 | 33 | it('should default to unauthenticated', inject([TestAuthService, LoggingService], 34 | (service: TestAuthService, loggingService: LoggingService) => { 35 | expect(service.isUserAuthenticated).toBe(false); 36 | })); 37 | 38 | it('should default username to empty', inject([TestAuthService, LoggingService], 39 | (service: TestAuthService, loggingService: LoggingService) => { 40 | expect(service.userName).toEqual(''); 41 | })); 42 | 43 | it('can call login', inject([TestAuthService, LoggingService], 44 | (service: TestAuthService, loggingService: LoggingService) => { 45 | spyOn(service, 'login'); 46 | service.login(); 47 | expect(service.login).toHaveBeenCalled(); 48 | })); 49 | 50 | it('can call logout', inject([TestAuthService, LoggingService], 51 | (service: TestAuthService, loggingService: LoggingService) => { 52 | spyOn(service, 'logout'); 53 | service.logout(); 54 | expect(service.logout).toHaveBeenCalled(); 55 | })); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/framework/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AdalService } from 'adal-angular4'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class AuthService { 8 | _isUserAuthenticated: boolean; 9 | _userName: string; 10 | 11 | constructor(private adalService: AdalService) { 12 | } 13 | 14 | get isUserAuthenticated() { 15 | return this.adalService.userInfo ? this.adalService.userInfo.authenticated : false; 16 | } 17 | 18 | get userName() { 19 | return this.adalService.userInfo ? this.adalService.userInfo.userName : ''; 20 | } 21 | 22 | login() { 23 | this.adalService.login(); 24 | } 25 | 26 | logout(): void { 27 | this.adalService.logOut(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/framework/services/authorization-data.service.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from './data.service'; 2 | import { ActionCode } from '../models/authorization.types'; 3 | 4 | export class AuthorizationDataService extends DataService { 5 | 6 | getPermissions(): Promise> { 7 | const endpoint = `${this.apiServer.metadata}authorizations`; 8 | return this.get>(endpoint); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/framework/services/authorization.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { AuthorizationService } from './authorization.service'; 3 | import { AuthorizationDataService } from './authorization-data.service'; 4 | import { MockAppConfig } from '../../demo-common/testing/testing-helpers'; 5 | import { AppConfig } from '../../app.config'; 6 | 7 | export class MockAuthorizationDataService { 8 | getPermissions() { 9 | return Promise.resolve(['VIEW']); 10 | } 11 | } 12 | 13 | describe('AuthorizationService', () => { 14 | beforeEach(() => { 15 | AppConfig.settings = MockAppConfig.settings; 16 | TestBed.configureTestingModule({ 17 | providers: [ 18 | AuthorizationService, 19 | { 20 | provide: AuthorizationDataService, 21 | useClass: MockAuthorizationDataService 22 | } 23 | ] 24 | }); 25 | }); 26 | 27 | it('should create the service', inject([AuthorizationService], (service: AuthorizationService) => { 28 | expect(service).toBeTruthy(); 29 | })); 30 | 31 | it('should authorize if has permission', inject([AuthorizationService], 32 | async (service: AuthorizationService) => { 33 | await service.initializePermissions(); 34 | expect(service.hasPermission('VIEW')).toBe(true); 35 | })); 36 | 37 | it('should not authorize if no permission', inject([AuthorizationService], 38 | async (service: AuthorizationService) => { 39 | await service.initializePermissions(); 40 | expect(service.hasPermission('UPDATE')).toBe(false); 41 | })); 42 | }); 43 | -------------------------------------------------------------------------------- /src/app/framework/services/authorization.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActionCode } from '../models/authorization.types'; 3 | import { AuthorizationDataService } from './authorization-data.service'; 4 | import { AppConfig } from '../../app.config'; 5 | 6 | @Injectable() 7 | export class AuthorizationService { 8 | 9 | permissions: Array; // The actions for which this user has permissions 10 | 11 | constructor(private authorizationDataService: AuthorizationDataService) { 12 | } 13 | 14 | hasPermission(action: ActionCode) { 15 | if (!AppConfig.settings.aad.requireAuth || !action) { 16 | return true; 17 | } 18 | if (this.permissions && this.permissions.find(permission => { 19 | return permission === action; 20 | })) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | 26 | initializePermissions() { 27 | return new Promise((resolve, reject) => { 28 | this.authorizationDataService.getPermissions() 29 | .then(permissions => { 30 | this.permissions = permissions; 31 | resolve(); 32 | }) 33 | .catch((e) => { 34 | reject(e); 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/framework/services/can-deactivate-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export interface ICanComponentDeactivate { 6 | canDeactivate: (component: ICanComponentDeactivate, 7 | route: ActivatedRouteSnapshot, 8 | state: RouterStateSnapshot, 9 | nextState?: RouterStateSnapshot) => boolean | Observable | Promise; 10 | } 11 | 12 | // If a component should be checked before navigating away, then this CanDeactivateGuardService 13 | // can be used in the route definition for that component 14 | @Injectable() 15 | export class CanDeactivateGuardService implements CanDeactivate { 16 | canDeactivate(component: ICanComponentDeactivate, 17 | route: ActivatedRouteSnapshot, 18 | state: RouterStateSnapshot, 19 | nextState?: RouterStateSnapshot 20 | ): Observable | Promise | boolean { 21 | if (!component || !component.canDeactivate) { 22 | return true; 23 | } 24 | return component.canDeactivate(component, route, state, nextState); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/framework/services/global-events.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class GlobalEventsService { 6 | private routingStartedSource = new Subject(); 7 | routingStarted = this.routingStartedSource.asObservable(); 8 | private routingCompleteSource = new Subject(); 9 | routingComplete = this.routingCompleteSource.asObservable(); 10 | private loadingDataStartedSource = new Subject(); 11 | loadingDataStarted = this.loadingDataStartedSource.asObservable(); 12 | private loadingDataCompleteSource = new Subject(); 13 | loadingDataComplete = this.loadingDataCompleteSource.asObservable(); 14 | private _isBusyRouting = false; 15 | 16 | get isBusyRouting() { 17 | return this._isBusyRouting; 18 | } 19 | 20 | startRouting() { 21 | this._isBusyRouting = true; 22 | this.routingStartedSource.next(); 23 | } 24 | 25 | completeRouting() { 26 | this._isBusyRouting = false; 27 | this.routingCompleteSource.next(); 28 | } 29 | 30 | startLoadingData() { 31 | this.loadingDataStartedSource.next(); 32 | } 33 | 34 | completeLoadingData() { 35 | this.loadingDataCompleteSource.next(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/framework/services/metadata-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DataService } from './data.service'; 3 | import { ILookupList, ITimePeriod } from '../models/metadata.models'; 4 | 5 | // AJAX calls related to metadata 6 | @Injectable() 7 | export class MetadataDataService extends DataService { 8 | 9 | getLookupList(name: string): Promise> { 10 | const endpoint = `${this.apiServer.metadata}lookuplists/${name}`; 11 | return this.get>(endpoint); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/framework/services/metadata.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MetadataDataService } from './metadata-data.service'; 3 | import { IPartsListChoice } from '../models/form-controls.models'; 4 | 5 | @Injectable() 6 | export class MetadataService { 7 | 8 | private dropdownLists: Array<{ name: string, items: Array }> = []; 9 | 10 | constructor(private metadataDataService: MetadataDataService) { 11 | } 12 | 13 | getLookupList(name: string) { 14 | const promise = new Promise((resolve, reject) => { 15 | const index = this.dropdownLists.findIndex(item => item.name === name); 16 | if (index > -1) { 17 | resolve(this.dropdownLists[index].items); 18 | } else { 19 | this.metadataDataService.getLookupList(name) 20 | .then(list => { 21 | const formattedList = Object.keys(list).map(key => { 22 | return { value: key, label: list[key] }; 23 | }); 24 | this.dropdownLists.push({ name: name, items: formattedList }); 25 | resolve(formattedList); 26 | }).catch((e) => { 27 | reject(e); 28 | }); 29 | } 30 | }); 31 | return promise; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/framework/services/system-message-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DataService } from './data.service'; 3 | 4 | @Injectable() 5 | export class SystemMessageDataService extends DataService { 6 | 7 | getMessage(id: number): Promise { 8 | const endpoint = `${this.apiServer.metadata}messages/${id}`; 9 | return this.get(endpoint); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/framework/services/system-message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { SystemMessageDataService } from './system-message-data.service'; 3 | import { SystemMessageService } from './system-message.service'; 4 | 5 | const testErrorMessage = 'Test error message'; 6 | const testErrorMessage2 = '{0} error {1}'; 7 | 8 | class MockSystemMessageDataService { 9 | getMessage(id: number, errorParameters?: Array): Promise { 10 | return new Promise((resolve) => { 11 | if (id === 2) { 12 | resolve(testErrorMessage2); 13 | } else { 14 | resolve(testErrorMessage); 15 | } 16 | }); 17 | } 18 | } 19 | 20 | describe('SystemMessageService', () => { 21 | beforeEach(() => { 22 | 23 | TestBed.configureTestingModule({ 24 | providers: [ 25 | SystemMessageService, 26 | { 27 | provide: SystemMessageDataService, 28 | useClass: MockSystemMessageDataService 29 | } 30 | ] 31 | }); 32 | }); 33 | 34 | it('should create the service', 35 | inject([SystemMessageService], async(service: SystemMessageService) => { 36 | expect(service).toBeTruthy(); 37 | })); 38 | }); 39 | -------------------------------------------------------------------------------- /src/app/framework/services/system-message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SystemMessageDataService } from './system-message-data.service'; 3 | 4 | @Injectable() 5 | export class SystemMessageService { 6 | private messages: Array<{ id: number, message: string }> = []; 7 | 8 | constructor(private systemMessageDataService: SystemMessageDataService) { 9 | } 10 | 11 | getMessage(id: number, errorParameters?: Array) { 12 | let message = `Invalid (Error: ${id})`; 13 | const match = this.messages.find(item => item.id === id); 14 | if (match) { 15 | message = match.message; 16 | if (errorParameters) { 17 | errorParameters.forEach((param, index) => { 18 | const token = `{${index.toString()}}`; 19 | message = message.replace(token, param); 20 | }); 21 | } 22 | } 23 | return message; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/framework/services/url-serializer.service.ts: -------------------------------------------------------------------------------- 1 | import { DefaultUrlSerializer, UrlTree } from '@angular/router'; 2 | 3 | export class LowerCaseUrlSerializer extends DefaultUrlSerializer { 4 | parse(url: string): UrlTree { 5 | return super.parse(url.toLowerCase()); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/framework/services/utilities.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { UtilitiesService } from './utilities.service'; 3 | 4 | describe('UtilitiesService', () => { 5 | beforeEach(() => { 6 | 7 | TestBed.configureTestingModule({ 8 | providers: [ 9 | UtilitiesService 10 | ] 11 | }); 12 | }); 13 | 14 | it('should create the service', inject([UtilitiesService], (service: UtilitiesService) => { 15 | expect(service).toBeTruthy(); 16 | })); 17 | 18 | it('should subtract a day from a date', () => { 19 | const testDate = new Date('04/22/1990'); 20 | const yesterday = UtilitiesService.subtractDays(testDate, 1); 21 | expect(yesterday.getDate()).toEqual(21); 22 | }); 23 | 24 | it('should add a day to a date', () => { 25 | const testDate = new Date('04/22/1990'); 26 | const tomorrow = UtilitiesService.addDays(testDate, 1); 27 | expect(tomorrow.getDate()).toEqual(23); 28 | }); 29 | 30 | it('can convert date to string', () => { 31 | const testDate = new Date('04/22/1990'); 32 | const result = UtilitiesService.convertDateToString(testDate, 'MM/DD/YYYY'); 33 | expect(result).toBe('04/22/1990'); 34 | }); 35 | 36 | it('can convert string to date', () => { 37 | const testDate = '04/22/1990'; 38 | const result = UtilitiesService.convertStringToDate(testDate, 'MM/DD/YYYY'); 39 | expect(result.getDate()).toBe(22); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/framework/services/web-storage.service.ts: -------------------------------------------------------------------------------- 1 | export class WebStorageService { 2 | 3 | static setLocalStorage(key: string, data: string): void { 4 | localStorage.setItem(key, data); 5 | } 6 | static getLocalStorage(key: string): string | null { 7 | return localStorage.getItem(key); 8 | } 9 | static removeLocalStorage(key: string) { 10 | localStorage.removeItem(key); 11 | } 12 | static setSessionStorage(key: string, data: string): void { 13 | sessionStorage.setItem(key, data); 14 | } 15 | static getSessionStorage(key: string): string | null { 16 | return sessionStorage.getItem(key); 17 | } 18 | static removeSessionStorage(key: string) { 19 | sessionStorage.removeItem(key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/framework/validation/models/server-error.models.ts: -------------------------------------------------------------------------------- 1 | export interface IServerError { 2 | details: Array<{ code: number, message: string, target: string }>; 3 | code: number; 4 | message: string; 5 | target: string; 6 | } 7 | 8 | export interface INavigationError { 9 | message?: string; 10 | serverError?: IServerError; 11 | navigatingTo: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/framework/validation/validation.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { ValidationService } from './services/validation.service'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule, 9 | HttpClientModule 10 | ], 11 | declarations: [ 12 | ], 13 | providers: [ 14 | ValidationService 15 | ], 16 | exports: [ 17 | ] 18 | }) 19 | export class ValidationModule { } 20 | 21 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Home Page

4 |

Welcome to a class inheritance demo

5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../scss/colors'; 2 | @import '../../scss/mixins'; 3 | 4 | .tile-container { 5 | flex-direction: column; 6 | } 7 | .tile { 8 | width: 100%; 9 | flex-direction: row; 10 | padding-top: 20px; 11 | } 12 | 13 | /* Small devices (tablets, 768px and up) */ 14 | @media (min-width: 768px) { 15 | .overflow { 16 | overflow: hidden; 17 | margin: 15px 0; 18 | img { 19 | height: auto; 20 | width: 100%; 21 | } 22 | } 23 | } 24 | 25 | @media (min-width: 992px) { 26 | .overflow { 27 | overflow: hidden; 28 | margin: 15px 0; 29 | img { 30 | height: 100%; 31 | width: auto; 32 | } 33 | } 34 | } 35 | 36 | .header { 37 | @include font-h(); 38 | margin-left: 8px; 39 | } 40 | 41 | a { 42 | &:hover { 43 | text-decoration: none; 44 | } 45 | } 46 | .well { 47 | background: #e9e9e9, !important; 48 | border-radius: 0 !important; 49 | position: relative; 50 | float: left; 51 | width: 100%; 52 | margin: 15px 0; 53 | border: 1px solid #e5e5e5; 54 | } 55 | h6, .h6{ 56 | font-family: Tahoma, Helvetica, Arial, sans-serif !important; 57 | font-size: 15px; 58 | font-weight: bold; 59 | } -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './home.component.html', 5 | styleUrls: ['./home.component.scss'] 6 | }) 7 | export class HomeComponent { 8 | } 9 | -------------------------------------------------------------------------------- /src/app/home/log-out/log-out.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Thanks for using this demo

5 |

6 | We are logging you out. 7 |

8 |
9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /src/app/home/log-out/log-out.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../scss/colors'; 2 | @import '../../../scss/mixins'; 3 | 4 | @include generic-button(); 5 | 6 | #post-btn { 7 | @include submit-button(); 8 | } 9 | 10 | .btn { 11 | margin-top: 8px; 12 | padding: 15px 18px 15px 18px; 13 | border-radius: 4px; 14 | } 15 | 16 | .well { 17 | padding: 40px 0 40px 0; 18 | } -------------------------------------------------------------------------------- /src/app/home/log-out/log-out.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { TestInjector } from '../../demo-common/testing/testing-helpers'; 3 | import { LogOutComponent } from './log-out.component'; 4 | import { AuthService } from '../../framework/services/auth.service'; 5 | 6 | describe('LogOutComponent', () => { 7 | let component: LogOutComponent; 8 | beforeAll(() => { 9 | TestInjector.setInjector(); 10 | }); 11 | 12 | beforeEach(() => { 13 | component = new LogOutComponent( 14 | TestInjector.getService(AuthService), 15 | null); 16 | }); 17 | 18 | it('should be created', async(() => { 19 | expect(component).toBeTruthy(); 20 | })); 21 | 22 | it('should log out upon creation', () => { 23 | spyOn(component, 'logout'); 24 | component.ngOnInit(); 25 | expect(component.logout).toHaveBeenCalled(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/home/log-out/log-out.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../framework/services/auth.service'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'la-log-out', 7 | templateUrl: './log-out.component.html', 8 | styleUrls: ['./log-out.component.scss'] 9 | }) 10 | export class LogOutComponent implements OnInit { 11 | 12 | constructor(private authService: AuthService, 13 | protected route: ActivatedRoute 14 | ) {} 15 | 16 | ngOnInit() { 17 | this.logout(); 18 | } 19 | 20 | logout() { 21 | this.authService.logout(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/home/page-error/page-error.component.html: -------------------------------------------------------------------------------- 1 |

{{errorMessage}}

2 | 3 | -------------------------------------------------------------------------------- /src/app/home/page-error/page-error.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/home/page-error/page-error.component.scss -------------------------------------------------------------------------------- /src/app/home/page-error/page-error.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestInjector } from '../../demo-common/testing/testing-helpers'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { async } from '@angular/core/testing'; 4 | import { PageErrorComponent } from './page-error.component'; 5 | import { of } from 'rxjs'; 6 | 7 | class MockActivatedRoute extends ActivatedRoute { 8 | queryParams = of({ 9 | errorMessage: 'Test message' 10 | }); 11 | } 12 | 13 | describe('PageErrorComponent', () => { 14 | let component: PageErrorComponent; 15 | beforeAll(() => { 16 | TestInjector.setInjector(); 17 | }); 18 | 19 | beforeEach(() => { 20 | component = new PageErrorComponent(new MockActivatedRoute()); 21 | }); 22 | 23 | it('should be created', async(() => { 24 | component.ngOnInit(); 25 | expect(component).toBeTruthy(); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/home/page-error/page-error.component.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute } from '@angular/router'; 2 | import { Component, OnInit } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'la-page-error', 6 | templateUrl: './page-error.component.html', 7 | styleUrls: ['./page-error.component.scss'] 8 | }) 9 | export class PageErrorComponent implements OnInit { 10 | 11 | errorMessage = 'Error occurred'; 12 | 13 | constructor(private route: ActivatedRoute) { } 14 | 15 | ngOnInit() { 16 | this.route.queryParams.subscribe((params) => { 17 | let errorMessage = params['errorMessage']; 18 | if (!errorMessage) { 19 | errorMessage = params['errormessage']; 20 | } 21 | if (errorMessage && (errorMessage).toLowerCase().indexOf('auth') !== -1) { 22 | this.errorMessage = 'Azure AD token has expired. Please logout and login again.'; 23 | } 24 | }); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/home/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Page does not exist

3 |
-------------------------------------------------------------------------------- /src/app/home/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/home/page-not-found/page-not-found.component.scss -------------------------------------------------------------------------------- /src/app/home/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async } from '@angular/core/testing'; 2 | import { TestInjector } from '../../demo-common/testing/testing-helpers'; 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | 5 | describe('PageNotFoundComponent', () => { 6 | let component: PageNotFoundComponent; 7 | beforeAll(() => { 8 | TestInjector.setInjector(); 9 | }); 10 | 11 | beforeEach(() => { 12 | component = new PageNotFoundComponent(); 13 | }); 14 | 15 | it('should be created', async(() => { 16 | expect(component).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/home/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent { 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/directives/accordion/accordion.component.css: -------------------------------------------------------------------------------- 1 | .ui-panel-titlebar.ui-widget-header.ui-helper-clearfix.ui-corner-all { 2 | background-color: white !important; 3 | } 4 | .ui-panel { 5 | margin-bottom: 15px; 6 | margin-top: 15px; 7 | } 8 | .ui-panel .ui-panel-titlebar .h3 { 9 | color: #000; 10 | text-transform: capitalize; 11 | font-family: Tahoma, Helvetica, Arial, sans-serif !important; 12 | font-size: 14px; 13 | font-weight: bold; 14 | } 15 | .ui-panel .ui-panel-titlebar { 16 | position: relative; 17 | font-size: 1.25em; 18 | } 19 | .edit { 20 | position: absolute; 21 | top: 10px; 22 | right: 60px; 23 | font-weight: bold; 24 | color: #333; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/directives/accordion/accordion.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{heading}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/shared/directives/accordion/accordion.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-accordion', 5 | templateUrl: './accordion.component.html', 6 | styleUrls: ['./accordion.component.css'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class AccordionComponent { 10 | @Input() heading: string; 11 | @Input() collapsed: boolean; 12 | @Input() disabled = false; 13 | @Input() visible = true; 14 | 15 | isCollapsed(): boolean { 16 | return this.collapsed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/directives/alert/alert.component.css: -------------------------------------------------------------------------------- 1 | .fa, .glyphicon { 2 | color: inherit !important; 3 | vertical-align: middle; 4 | } -------------------------------------------------------------------------------- /src/app/shared/directives/alert/alert.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | × 7 | 8 |
-------------------------------------------------------------------------------- /src/app/shared/directives/alert/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-alert', 5 | templateUrl: './alert.component.html', 6 | styleUrls: ['./alert.component.css'], 7 | }) 8 | export class AlertComponent { 9 | 10 | @Input() showAlert = true; 11 | @Input() alertType = 'danger'; 12 | 13 | toggle() { 14 | this.showAlert = !this.showAlert; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/directives/calendar/calendar.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/shared/directives/calendar/calendar.component.css -------------------------------------------------------------------------------- /src/app/shared/directives/calendar/calendar.component.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/shared/directives/calendar/calendar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { fakeAsync } from '@angular/core/testing'; 2 | import { CalendarModule } from 'primeng/components/calendar/calendar'; 3 | import { CalendarComponent } from './calendar.component'; 4 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 5 | 6 | describe('CalendarComponent', () => { 7 | let component: CalendarComponent; 8 | beforeAll(() => { 9 | TestInjector.setInjector(); 10 | }); 11 | 12 | beforeEach(() => { 13 | component = new CalendarComponent(); 14 | }); 15 | 16 | it('should create', () => { 17 | expect(component).toBeTruthy(); 18 | }); 19 | 20 | it('should emit the selected date', fakeAsync((): void => { 21 | const testDate = new Date('04/22/1989'); 22 | spyOn(component.dateSelected, 'emit'); 23 | component.selectedDate = testDate; 24 | component.onSelected(); 25 | expect(component.dateSelected.emit).toHaveBeenCalledWith(testDate); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/directives/calendar/calendar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; 2 | 3 | // This calendar component is for a standalone value 4 | // If the date is a field on a form, use the form-field control instead 5 | @Component({ 6 | selector: 'la-calendar', 7 | templateUrl: './calendar.component.html', 8 | styleUrls: ['./calendar.component.css'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class CalendarComponent implements OnInit { 12 | yearRange: string; 13 | @Input() selectedDate: Date; 14 | @Input() placeholder: string; 15 | @Input() minDate: Date; 16 | @Input() maxDate: Date; 17 | @Input() showCalendarOnFocus = false; 18 | @Input() hideMonthNavigator = false; 19 | @Input() hideYearNavigator = false; 20 | @Input() disabled = false; 21 | @Output() dateSelected: EventEmitter = new EventEmitter(); 22 | 23 | ngOnInit() { 24 | this.yearRange = '1900:' + ((new Date()).getFullYear() + 10).toString(); 25 | } 26 | 27 | onSelected() { 28 | this.dateSelected.emit(this.selectedDate); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/directives/confirm-dialog/confirm-dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/app/shared/directives/confirm-dialog/confirm-dialog.component.css -------------------------------------------------------------------------------- /src/app/shared/directives/confirm-dialog/confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 | 3 |

{{message}}

4 | 5 | 6 |
7 | 10 | 13 |
14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /src/app/shared/directives/confirm-dialog/confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation, Output, EventEmitter } from '@angular/core'; 2 | import { ConfirmChoice } from '../../models/confirm-choices.enum'; 3 | 4 | @Component({ 5 | selector: 'la-confirm-dialog', 6 | templateUrl: './confirm-dialog.component.html', 7 | styleUrls: ['./confirm-dialog.component.css'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class ConfirmDialogComponent { 11 | display = false; 12 | @Input() title = 'Please confirm.'; 13 | @Input() message = 'Do you want to save or discard your changes?'; 14 | @Input() okButtonText = 'Save'; 15 | @Input() cancelButtonText = 'Cancel'; 16 | @Input() allowSave = true; 17 | @Output() confirmed: EventEmitter = new EventEmitter(); 18 | 19 | show() { 20 | this.display = true; 21 | } 22 | save() { 23 | this.display = false; 24 | this.confirmed.emit(ConfirmChoice.save); 25 | } 26 | cancel() { 27 | if (this.display) { 28 | this.display = false; 29 | this.confirmed.emit(ConfirmChoice.cancel); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/shared/directives/confirm-dialog/parts-confirm-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 2 | import { fakeAsync } from '@angular/core/testing'; 3 | import { DialogModule, MenuModule, ButtonModule } from 'primeng/primeng'; 4 | import { ConfirmDialogComponent } from './confirm-dialog.component'; 5 | import { ConfirmChoice } from '../../models/confirm-choices.enum'; 6 | 7 | describe('ConfirmDialogComponent', () => { 8 | let component: ConfirmDialogComponent; 9 | beforeAll(() => { 10 | TestInjector.setInjector(); 11 | }); 12 | 13 | beforeEach(() => { 14 | component = new ConfirmDialogComponent(); 15 | }); 16 | 17 | it('should create', () => { 18 | expect(component).toBeTruthy(); 19 | }); 20 | 21 | it('show should display', () => { 22 | expect(component.display).toBeFalsy(); 23 | component.show(); 24 | expect(component.display).toBeTruthy(); 25 | }); 26 | 27 | it('cancel should hide', () => { 28 | component.show(); 29 | component.cancel(); 30 | expect(component.display).toBeFalsy(); 31 | }); 32 | 33 | it('save should hide', () => { 34 | component.show(); 35 | component.save(); 36 | expect(component.display).toBeFalsy(); 37 | }); 38 | 39 | it('should emit Save on save', fakeAsync((): void => { 40 | component.confirmed.subscribe((choice: ConfirmChoice) => { 41 | expect(choice).toEqual(ConfirmChoice.save); 42 | }); 43 | component.show(); 44 | component.save(); 45 | })); 46 | 47 | it('should not emit Cancel on save', fakeAsync((): void => { 48 | component.confirmed.subscribe((choice: ConfirmChoice) => { 49 | expect(choice === ConfirmChoice.cancel).toBeFalsy(); 50 | }); 51 | component.show(); 52 | component.save(); 53 | })); 54 | 55 | it('should emit Cancel on cancel', fakeAsync((): void => { 56 | component.confirmed.subscribe((choice: ConfirmChoice) => { 57 | expect(choice).toEqual(ConfirmChoice.cancel); 58 | }); 59 | component.show(); 60 | component.cancel(); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/shared/directives/data-table/data-table-models.ts: -------------------------------------------------------------------------------- 1 | export interface IDataTableColumn { 2 | name: string; 3 | header?: string; 4 | showTotal?: boolean; 5 | sortable?: boolean; 6 | editable?: boolean; 7 | filter?: boolean; 8 | mask?: string; 9 | numberFormat?: string; 10 | dataType?: 'string' | 'number' | 'boolean' | 'date' | 'choice' | 'url' | 'mask'; 11 | options?: Array<{ label: string, value: any }>; 12 | filterOptions?: Array<{ label: string, value: any }>; 13 | link?: string; 14 | linkText?: string; 15 | width?: string; 16 | hidden?: boolean; 17 | multisortOrder?: number; 18 | sortDesc?: boolean; 19 | includeEmptyChoice?: boolean; 20 | emptyChoiceLabel?: string; 21 | excludeFromGlobalFilter?: boolean; 22 | } 23 | 24 | export interface IDataTableRowExpansionField { 25 | label: string; 26 | fieldName: string; 27 | dataType?: 'string' | 'number' | 'boolean' | 'date' | 'choice' ; 28 | options?: Array<{ label: string, value: any }>; 29 | editable?: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/directives/dialog/dialog.component.css: -------------------------------------------------------------------------------- 1 | .ui-dialog { 2 | border-radius: 0 !important; 3 | position: fixed; 4 | 5 | } 6 | .ui-dialog-title { 7 | font-weight: normal, !important; 8 | } 9 | .ui-dialog-titlebar-close .fa, 10 | .ui-dialog-titlebar-close .glyphicon { 11 | color: inherit !important; 12 | font-size: 1.5em; 13 | line-height: 1.5em; 14 | } 15 | .ui-dialog-titlebar-close .fa-close:before, 16 | .ui-dialog-titlebar-close .glyphicon-remove:before { 17 | content: 'X' !important; 18 | font-family: Helvetica, Arial, Tahoma, sans-serif; 19 | } 20 | .ui-dialog-buttonpane { 21 | text-align: left !important; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/directives/dialog/dialog.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/shared/directives/dialog/dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-dialog', 5 | templateUrl: './dialog.component.html', 6 | styleUrls: ['./dialog.component.css'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class DialogComponent { 10 | @Input() visible = false; 11 | @Input() title = 'Please confirm.'; 12 | @Input() width: any; 13 | @Input() height: any; 14 | @Output() onHide: EventEmitter = new EventEmitter(); 15 | @Output() onShow: EventEmitter = new EventEmitter(); 16 | 17 | show() { 18 | this.visible = true; 19 | } 20 | 21 | showDialog() { 22 | this.onShow.emit(); 23 | } 24 | 25 | hide() { 26 | this.onHide.emit(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/directives/disable-if-unauthorized/disable-if-unauthorized.directive.ts: -------------------------------------------------------------------------------- 1 | import { OnInit } from '@angular/core'; 2 | import { Directive, ElementRef, Input } from '@angular/core'; 3 | import { ActionCode } from '../../../framework/models/authorization.types'; 4 | import { AuthorizationService } from '../../../framework/services/authorization.service'; 5 | 6 | @Directive({ 7 | selector: '[laDisableIfUnauthorized]' 8 | }) 9 | export class DisableIfUnauthorizedDirective implements OnInit { 10 | @Input('laDisableIfUnauthorized') permission: ActionCode; 11 | 12 | constructor(private el: ElementRef, private authorizationService: AuthorizationService) { 13 | } 14 | 15 | ngOnInit() { 16 | if (!this.authorizationService.hasPermission(this.permission)) { 17 | this.el.nativeElement.disabled = true; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message/error-message.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{header}}

3 |
    4 |
  • 5 | {{error}} 6 |
  • 7 |
8 | 9 | {{errorList}} 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message/error-message.component.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | margin-top: 0px; 3 | } 4 | li { 5 | font-size: 14px; 6 | font-family: Tahoma, Helvetica, Arial, sans-serif; 7 | font-weight: normal; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message/error-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessageComponent } from './error-message.component'; 2 | import { TestInjector } from '../../../demo-common/testing/testing-helpers'; 3 | 4 | describe('ErrorMessageComponent', () => { 5 | let component: ErrorMessageComponent; 6 | beforeAll(() => { 7 | TestInjector.setInjector(); 8 | }); 9 | 10 | beforeEach(() => { 11 | component = new ErrorMessageComponent(); 12 | }); 13 | it('should be created', () => { 14 | expect(component).toBeTruthy(); 15 | }); 16 | 17 | it('can set errorList', () => { 18 | component.errorList = ['test']; 19 | expect(component.errorList.length).toBe(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message/error-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'la-error-message', 5 | templateUrl: './error-message.component.html', 6 | styleUrls: ['./error-message.component.scss'] 7 | }) 8 | export class ErrorMessageComponent { 9 | 10 | @Input() errorList: Array = []; 11 | @Input() header: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/directives/form-field/form-field.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | @import '../../../../scss/dimensions'; 4 | 5 | .inline-header { 6 | display: inline-block; 7 | vertical-align: baseline; 8 | } 9 | /* calendar */ 10 | .date-list { 11 | width:65px; 12 | display:inline-block; 13 | } 14 | .ui-calendar { 15 | display: block; 16 | } 17 | .ui-calendar input { 18 | width: 100%; 19 | } 20 | .readOnly { 21 | height: 34px; 22 | padding: 6px 12px; 23 | color: #555555; 24 | background-color: #eeeeee; 25 | border: 1px solid #ccc; 26 | border-radius: 4px; 27 | position: relative; 28 | font-size: 1em; 29 | top: 2px; 30 | } 31 | .fa { 32 | color: white; 33 | } 34 | 35 | .topAlignLabel { 36 | vertical-align: top; 37 | } 38 | 39 | p-inputmask .ui-inputtext { 40 | display: block; 41 | width: 100%; 42 | height: 34px; 43 | padding: 6px 12px; 44 | font-size: 1em; 45 | color: #555555; 46 | background-color: #fff; 47 | background-image: none; 48 | border: 1px solid #ccc; 49 | border-radius: 4px; 50 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 51 | transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 52 | } 53 | 54 | .form-group { 55 | p, label, .label, legend, .legend { 56 | @include font-p(); 57 | font-weight: normal; 58 | } 59 | p.readOnly { 60 | font-weight: normal; 61 | } 62 | 63 | input { 64 | margin-bottom: 10px; 65 | } 66 | } 67 | 68 | select.form-control { 69 | margin-bottom: 10px; 70 | height:34px; 71 | } 72 | 73 | .ui-calendar.ui-calendar-w-btn input { 74 | height:34px; 75 | } 76 | 77 | .ui-calendar button { 78 | max-height: $calendar-button-height; 79 | width: $calendar-button-width; 80 | right: 0; 81 | } 82 | .hide-labels label { 83 | position: absolute; 84 | height: 1px; 85 | width: 1px; 86 | overflow: hidden; 87 | text-indent: -9999px; 88 | } 89 | 90 | .calendar-input-control { 91 | span { 92 | input { 93 | width: calc(100% - #{$calendar-button-width}); //interpolate sass variable into calc 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/app/shared/directives/form-list/form-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

{{label}}

6 |

9 | {{showSelectedValue()}} 10 |

11 | 12 | 20 | 25 | 26 | 32 | 33 | 34 |
36 |
37 | {{error}} 38 |
39 |
40 |
41 |
42 |
-------------------------------------------------------------------------------- /src/app/shared/directives/form-list/form-list.component.scss: -------------------------------------------------------------------------------- 1 | .inline-header { 2 | display: inline-block; 3 | vertical-align: baseline; 4 | } 5 | .readOnly { 6 | height: 100%; 7 | min-height: 34px; 8 | padding: 6px 12px; 9 | color: #555555; 10 | background-color: #eeeeee; 11 | border: 1px solid #ccc; 12 | border-radius: 4px; 13 | position: relative; 14 | font-size: 1em; 15 | overflow: hidden; 16 | white-space: nowrap; 17 | top: 2px; 18 | } 19 | .hide-labels p.label { 20 | position: absolute; 21 | height: 1px; 22 | width: 1px; 23 | overflow: hidden; 24 | text-indent: -9999px; 25 | } -------------------------------------------------------------------------------- /src/app/shared/directives/hide-if-unauthorized/hide-if-unauthorized.directive.ts: -------------------------------------------------------------------------------- 1 | import { OnInit } from '@angular/core'; 2 | import { Directive, ElementRef, Input } from '@angular/core'; 3 | import { ActionCode } from '../../../framework/models/authorization.types'; 4 | import { AuthorizationService } from '../../../framework/services/authorization.service'; 5 | 6 | @Directive({ 7 | selector: '[laHideIfUnauthorized]' 8 | }) 9 | export class HideIfUnauthorizedDirective implements OnInit { 10 | @Input('laHideIfUnauthorized') permission: ActionCode; 11 | 12 | constructor(private el: ElementRef, private authorizationService: AuthorizationService) { 13 | } 14 | 15 | ngOnInit() { 16 | if (!this.authorizationService.hasPermission(this.permission)) { 17 | this.el.nativeElement.style.display = 'none'; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/directives/input/input.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | @import '../../../../scss/dimensions'; 4 | 5 | .inline-header { 6 | display: inline-block; 7 | vertical-align: baseline; 8 | } 9 | /* calendar */ 10 | .date-list { 11 | width:65px; 12 | display:inline-block; 13 | } 14 | .ui-calendar { 15 | display: block; 16 | } 17 | .ui-calendar input { 18 | width: 100%; 19 | } 20 | .readOnly { 21 | height: 34px; 22 | padding: 6px 12px; 23 | color: #555555; 24 | background-color: #eeeeee; 25 | border: 1px solid #ccc; 26 | border-radius: 4px; 27 | position: relative; 28 | font-size: 1em; 29 | top: 2px; 30 | } 31 | .fa { 32 | color: white; 33 | } 34 | 35 | .topAlignLabel { 36 | vertical-align: top; 37 | } 38 | 39 | p-inputmask .ui-inputtext { 40 | display: block; 41 | width: 100%; 42 | height: 34px; 43 | padding: 6px 12px; 44 | font-size: 1em; 45 | color: #555555; 46 | background-color: #fff; 47 | background-image: none; 48 | border: 1px solid #ccc; 49 | border-radius: 4px; 50 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 51 | transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 52 | } 53 | 54 | .form-group { 55 | p, label, .label, legend, .legend { 56 | @include font-p(); 57 | font-weight: bold; 58 | } 59 | p.readOnly { 60 | font-weight: normal; 61 | } 62 | 63 | input { 64 | margin-bottom: 10px; 65 | } 66 | } 67 | 68 | select.form-control { 69 | margin-bottom: 10px; 70 | height:34px; 71 | } 72 | 73 | .ui-calendar.ui-calendar-w-btn input { 74 | height:34px; 75 | } 76 | 77 | .ui-calendar button { 78 | max-height: $calendar-button-height; 79 | width: $calendar-button-width; 80 | right: 0; 81 | } 82 | .hide-labels label { 83 | position: absolute; 84 | height: 1px; 85 | width: 1px; 86 | overflow: hidden; 87 | text-indent: -9999px; 88 | } 89 | 90 | .calendar-input-control { 91 | span { 92 | input { 93 | width: calc(100% - #{$calendar-button-width}); //interpolate sass variable into calc 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/app/shared/directives/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/shared/directives/menu/menu.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../scss/colors'; 2 | @import '../../../../scss/mixins'; 3 | 4 | body .ui-menu.ui-menubar { 5 | background-color: inherit; 6 | font-family: inherit; 7 | border: 0; 8 | } 9 | .ui-menu .ui-menuitem-link .ui-menuitem-icon { 10 | display: none; 11 | } 12 | .ui-menu .ui-menuitem-link { 13 | width: 100% !important; 14 | padding: .25em !important; 15 | } 16 | body .ui-menu .ui-menu-parent .ui-submenu-icon { 17 | position: relative; 18 | } 19 | .ui-menuitem-link:hover .ui-submenu-icon, .ui-menuitem-active .ui-submenu-icon, .ui-menuitem-active .ui-menuitem-text, .ui-megamenu-panel li:hover .ui-menuitem-text { 20 | color: #fff; 21 | } 22 | .ui-megamenu-panel .ui-menuitem-text { 23 | color: #333; 24 | } 25 | body .ui-menu .ui-menu-list .ui-menuitem .ui-menuitem-link:hover, body .ui-menu .ui-menu-list .ui-menuitem.ui-menuitem-active > .ui-menuitem-link { 26 | background-color: $link-blue; 27 | } 28 | .ui-megamenu-panel { 29 | position: absolute; 30 | z-index: 5000 !important; 31 | } 32 | .ui-megamenu-panel .ui-g .ui-g-12 { 33 | padding: 10px; 34 | } 35 | .ui-megamenu-panel.ui-corner-all { 36 | border-radius: 10; 37 | } 38 | body .ui-panelmenu .ui-panelmenu-header a { 39 | @include link-menu(); 40 | } 41 | body .ui-panelmenu .ui-panelmenu-header.ui-state-active a > .ui-menuitem-text { 42 | color: white; 43 | } 44 | body .ui-panelmenu .ui-panelmenu-content .ui-menuitem-link { 45 | @include link-menu(); 46 | } 47 | .ui-state-active .ui-menuitem-link { 48 | span.ui-menuitem-text { 49 | color: white; 50 | } 51 | } 52 | .ui-menu-list { 53 | top: 40px !important; 54 | } 55 | .logout { 56 | top: -30px !important; 57 | ul { 58 | li { 59 | a { 60 | span { 61 | @include link-logout(); 62 | text-transform: uppercase; 63 | } 64 | 65 | &:hover { 66 | span { 67 | color: white !important; 68 | text-transform: uppercase; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/shared/directives/menu/menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MegaMenuModule } from 'primeng/components/megamenu/megamenu'; 3 | import { TieredMenuModule } from 'primeng/components/tieredmenu/tieredmenu'; 4 | import { PanelMenuModule } from 'primeng/components/panelmenu/panelmenu'; 5 | import { MenuModule, MenuItem } from 'primeng/primeng'; 6 | import { MenuComponent } from './menu.component'; 7 | 8 | describe('MenuComponent', () => { 9 | let component: MenuComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | MenuModule, 16 | MegaMenuModule, 17 | TieredMenuModule, 18 | PanelMenuModule 19 | ], 20 | declarations: [ 21 | MenuComponent 22 | ] 23 | }) 24 | .compileComponents(); 25 | })); 26 | 27 | beforeEach(() => { 28 | fixture = TestBed.createComponent(MenuComponent); 29 | component = fixture.componentInstance; 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | 36 | it('can show popup', () => { 37 | component.type = 'popup'; 38 | fixture.detectChanges(); 39 | expect(component.popupMenu).toBeTruthy(); 40 | expect(component.logoutMenu).toBeFalsy(); 41 | }); 42 | 43 | it('can show logout menu', () => { 44 | component.type = 'logout'; 45 | fixture.detectChanges(); 46 | expect(component.popupMenu).toBeFalsy(); 47 | expect(component.logoutMenu).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/shared/directives/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation, ViewChild, OnInit, OnChanges, ChangeDetectorRef } from '@angular/core'; 2 | import { Menu, MenuItem } from 'primeng/primeng'; 3 | 4 | @Component({ 5 | selector: 'la-menu', 6 | templateUrl: './menu.component.html', 7 | styleUrls: ['./menu.component.scss'], 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class MenuComponent implements OnInit, OnChanges { 11 | @Input() items: MenuItem[]; 12 | @Input() type: string; 13 | @Input() orientation = 'horizontal'; 14 | filteredItems: MenuItem[]; 15 | 16 | @ViewChild('popupMenu', { static: false }) popupMenu: Menu; 17 | @ViewChild('logoutMenu', { static: false }) logoutMenu: Menu; 18 | 19 | constructor(private changeDetectorRef: ChangeDetectorRef) { 20 | } 21 | 22 | ngOnInit() { 23 | this.filterMenuItems(); 24 | } 25 | 26 | ngOnChanges() { 27 | this.filterMenuItems(); 28 | } 29 | 30 | toggle(event) { 31 | if (this.popupMenu) { 32 | this.popupMenu.toggle(event); 33 | } 34 | if (this.logoutMenu) { 35 | this.logoutMenu.toggle(event); 36 | } 37 | } 38 | 39 | filterMenuItems() { 40 | // Need to detect changes in the menu to trigger call to ngOnChanges(), 41 | // but do not trigger while already building the menu 42 | this.changeDetectorRef.detach(); 43 | const filteredItems = this.removeNonVisibleItems(this.items); 44 | this.filteredItems = filteredItems; 45 | this.changeDetectorRef.reattach(); 46 | } 47 | 48 | private removeNonVisibleItems(menu: MenuItem | MenuItem[]) { 49 | if (menu instanceof Array) { 50 | menu = menu.filter(item => { 51 | return item.visible === undefined || item.visible; 52 | }).map((item) => { 53 | return this.removeNonVisibleItems(item); 54 | }); 55 | } else if (menu && menu.items) { 56 | menu.items = (menu.items).filter(item => { 57 | return item.visible === undefined || item.visible; 58 | }).map((item) => { 59 | return this.removeNonVisibleItems(item); 60 | }); 61 | } 62 | return menu; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/shared/models/app-monitor.ts: -------------------------------------------------------------------------------- 1 | import { SeverityLevel } from '../../framework/logging/severity-level.model'; 2 | 3 | export interface IAppMonitor { 4 | trackEvent(event: {name: string}, customProperties?: { 5 | [key: string]: any; 6 | }): void; 7 | trackPageView(pageView: { 8 | name?: string; 9 | uri?: string; 10 | refUri?: string; 11 | pageType?: string; 12 | isLoggedIn?: boolean; 13 | properties?: { 14 | duration?: number; 15 | [key: string]: any; 16 | } 17 | }): void; 18 | trackPageViewPerformance(pageViewPerformance: { 19 | name?: string; 20 | uri?: string; 21 | perfTotal?: string; 22 | duration?: string; 23 | networkConnect?: string; 24 | sentRequest?: string; 25 | receivedResponse?: string; 26 | domProcessing?: string; 27 | }): void; 28 | trackException(exception: { exception: Error, severityLevel?: SeverityLevel | number; }): void; 29 | trackTrace(trace: {message: string}, customProperties?: { 30 | [key: string]: any; 31 | }): void; 32 | trackMetric(metric: { name: string, average: number }, customProperties?: { 33 | [key: string]: any; 34 | }): void; 35 | startTrackPage(name?: string): void; 36 | stopTrackPage(name?: string, url?: string, customProperties?: { 37 | [key: string]: any; 38 | }, measurements?: { 39 | [key: string]: number; 40 | }): void; 41 | startTrackEvent(name?: string): void; 42 | stopTrackEvent(name: string, properties?: { 43 | [key: string]: string; 44 | }, measurements?: { 45 | [key: string]: number; 46 | }): void; 47 | flush(async?: boolean): void; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shared/models/confirm-choices.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ConfirmChoice { 2 | save, 3 | discard, 4 | cancel 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/pipes/date-to-string.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { DateToStringPipe } from './date-to-string.pipe'; 2 | 3 | describe('DateToStringPipe', () => { 4 | it('can format a date to shortDate', () => { 5 | const testValue = new Date(2001, 0, 2); 6 | const pipe = new DateToStringPipe(); 7 | const result = pipe.transform(testValue, 'shortDate'); 8 | 9 | expect(result).toEqual('01/02/01'); 10 | }); 11 | 12 | it('can format a date to as MM/DD/YYYY', () => { 13 | const testValue = new Date(2001, 0, 2); 14 | const pipe = new DateToStringPipe(); 15 | const result = pipe.transform(testValue, 'MM/DD/YYYY'); 16 | 17 | expect(result).toEqual('01/02/2001'); 18 | }); 19 | 20 | 21 | it('can return empty on invalid date', () => { 22 | const testValue = 'pancakes'; 23 | const pipe = new DateToStringPipe(); 24 | const result = pipe.transform(testValue, 'shortDate'); 25 | 26 | expect(result).toEqual(''); 27 | }); 28 | 29 | it('can format MM-DD', () => { 30 | const testValue = new Date(2001, 0, 2); 31 | const pipe = new DateToStringPipe(); 32 | const result = pipe.transform(testValue, 'MM-DD'); 33 | 34 | expect(result).toEqual('01-02'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/shared/pipes/date-to-string.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { UtilitiesService } from '../../framework/services/utilities.service'; 3 | 4 | @Pipe({name: 'dateToString'}) 5 | export class DateToStringPipe implements PipeTransform { 6 | transform(value: Date, dateFormat: string) { 7 | if (!UtilitiesService.isDate(value)) { 8 | return ''; 9 | } 10 | if (dateFormat === 'shortDate') { 11 | dateFormat = 'MM/DD/YY'; 12 | } 13 | value.setMinutes(0, 0, 0); 14 | return UtilitiesService.convertDateToString(value, dateFormat); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/pipes/display-error.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'displayError' 5 | }) 6 | export class DisplayErrorPipe implements PipeTransform { 7 | 8 | transform(value: Object): Array { 9 | if (!value) { 10 | return null; 11 | } 12 | const errorMessages: Array = []; 13 | Object.keys(value).forEach(key => { 14 | if (!errorMessages.includes(value[key])) { 15 | errorMessages.push(value[key]); 16 | } 17 | }); 18 | 19 | return errorMessages; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/config/config.deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "name": "#{envName}" 4 | }, 5 | "appInsights": { 6 | "instrumentationKey": "#{appInsightsKey}" 7 | }, 8 | "logging": { 9 | "console": true, 10 | "appInsights": true, 11 | "traceEnabled": false 12 | }, 13 | "aad": { 14 | "requireAuth": true, 15 | "tenant": "#{aadTenant}", 16 | "clientId": "#{aadClientId}", 17 | "endpoints": { 18 | "api": "#{aadClientId}" 19 | } 20 | }, 21 | "apiServer": { 22 | "metadata": "#{medtadataApi}", 23 | "rules": "#{rulesApi}", 24 | "transaction": "#{transactionApi}" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/config/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "name": "DEV" 4 | }, 5 | "appInsights": { 6 | "instrumentationKey": "" 7 | }, 8 | "logging": { 9 | "console": true, 10 | "appInsights": false, 11 | "traceEnabled": false 12 | }, 13 | "aad": { 14 | "requireAuth": false, 15 | "tenant": "microsoft.onmicrosoft.com", 16 | "clientId": "", 17 | "endpoints": { 18 | "api": "" 19 | } 20 | }, 21 | "apiServer": { 22 | "metadata": "https://ng-demo-metadata.azurewebsites.net/api/", 23 | "rules": "https://ng-demo-rules.azurewebsites.net/api/", 24 | "transaction": "https://ng-demo-transaction.azurewebsites.net/api/" 25 | } 26 | } -------------------------------------------------------------------------------- /src/assets/config/config.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "name": "LOCAL" 4 | }, 5 | "appInsights": { 6 | "instrumentationKey": "" 7 | }, 8 | "logging": { 9 | "console": true, 10 | "appInsights": false, 11 | "traceEnabled": false 12 | }, 13 | "aad": { 14 | "requireAuth": false, 15 | "tenant": "microsoft.onmicrosoft.com", 16 | "clientId": "", 17 | "endpoints": { 18 | "api": "" 19 | } 20 | }, 21 | "apiServer": { 22 | "metadata": "http://localhost:56787/api/", 23 | "rules": "http://localhost:54084/api/", 24 | "transaction": "http://localhost:60101/api/" 25 | } 26 | } -------------------------------------------------------------------------------- /src/assets/config/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "name": "PROD" 4 | }, 5 | "appInsights": { 6 | "instrumentationKey": "" 7 | }, 8 | "logging": { 9 | "console": true, 10 | "appInsights": true, 11 | "traceEnabled": false 12 | }, 13 | "aad": { 14 | "requireAuth": true, 15 | "tenant": "microsoft.onmicrosoft.com", 16 | "clientId": "", 17 | "endpoints": { 18 | "api": "" 19 | } 20 | }, 21 | "apiServer": { 22 | "metadata": "https://ng-demo-metadata.azurewebsites.net/api/", 23 | "rules": "https://ng-demo-rules.azurewebsites.net/api/", 24 | "transaction": "https://ng-demo-transaction.azurewebsites.net/api/" 25 | } 26 | } -------------------------------------------------------------------------------- /src/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/assets/loading.gif -------------------------------------------------------------------------------- /src/environments/environment.deploy.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | name: 'deploy' 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | name: 'prod' 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | name: 'dev' 3 | }; 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurieatkinson/ng-patterns-demo/de5af7a0db5c7578ad6bda4cca1373557c25559a/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Patterns Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Loading...

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { environment } from 'environments/environment'; 4 | import { AppModule } from 'app/app.module'; 5 | import { AppInjector } from 'app/app-injector.service'; 6 | 7 | if (environment.name !== 'dev') { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule).then((moduleRef) => { 12 | AppInjector.getInstance().setInjector(moduleRef.injector); 13 | }); 14 | 15 | // git remote set-url --add --push origin https://laurieatkinson.visualstudio.com/NgPatternsDemo/_git/NgPatternsDemo 16 | // git remote set-url --add --push origin https://github.com/laurieatkinson/ng-patterns-demo.git 17 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular-CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | 70 | (window as any).global = window; 71 | -------------------------------------------------------------------------------- /src/scss/colors.scss: -------------------------------------------------------------------------------- 1 | $main-blue: #004d72; 2 | $main-green: #46850f; 3 | $main-gray: #999; 4 | 5 | $nav-blue: #005B80; 6 | $nav-active: #BED5E6; 7 | $nav-hover: #0093CE; 8 | 9 | $background: #efefef; 10 | $canvas: #fff; 11 | 12 | $footer: #cecece; 13 | 14 | $link-blue: #007bc2; 15 | 16 | $gray-dark: #c8c6c6; 17 | $gray-med-dark: #e9e9e9; 18 | $gray-med: #e5e5e5; 19 | $gray-light: #f5f5f5; 20 | $gray-blue: #F1F4F7; 21 | 22 | $text-black: #000; 23 | $text-dark: #333; 24 | $text-med: #666; 25 | $text-light: #999; 26 | 27 | $button-gradiant-gray: #ececec; 28 | $datatable-border: #c7c7c7; 29 | 30 | $prime-blue: #0275d8; 31 | $active-blue: #337ab7; 32 | $hover-gray: #f4f3f4; -------------------------------------------------------------------------------- /src/scss/dimensions.scss: -------------------------------------------------------------------------------- 1 | $calendar-button-width:28px; 2 | $calendar-button-height:34px; 3 | $tile-fixed-height:400px; -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | getTestBed().initTestEnvironment( 23 | BrowserDynamicTestingModule, 24 | platformBrowserDynamicTesting() 25 | ); 26 | // Then we find all the tests. 27 | const context = require.context('./', true, /\.spec\.ts$/); 28 | // And load the modules. 29 | context.keys().map(context); 30 | // Finally, start Karma to run the tests. 31 | __karma__.start(); 32 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts", 21 | "**/testing/*.ts" 22 | ] 23 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/spec", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [ 17 | "jasmine", 18 | "node" 19 | ] 20 | }, 21 | "files": [ 22 | "test.ts", 23 | "polyfills.ts" 24 | ], 25 | "include": [ 26 | "**/*.spec.ts", 27 | "**/testing/*.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/variables.less: -------------------------------------------------------------------------------- 1 | @import "../node_modules/bootstrap/less/bootstrap.less"; 2 | 3 | // COLORS 4 | 5 | // Grayscale 6 | @jh-black: #000; 7 | @jh-gray-darkest: #333; 8 | @jh-gray-darker: #666; 9 | @jh-gray-dark: #999; 10 | @jh-gray: #cecece; 11 | @jh-gray-light: #e9e9e9; 12 | @jh-gray-lighter: #e5e5e5; 13 | @jh-gray-lightest: #f5f5f5; 14 | @jh-white: #fff; 15 | 16 | // Color 17 | @jh-blue-dark: #004d72; 18 | @jh-blue: #007bc2; 19 | @jh-blue-light: #6aa0d4;; 20 | @jh-green: #46850f; 21 | @jh-green-light: #9da520; 22 | @jh-red: #d51923; 23 | @jh-red-light: #e86456; 24 | @jh-orange-dark: #ea621e; 25 | @jh-orange: #f6992e; 26 | @jh-orange-light: #fabc6d; 27 | 28 | // FONTS 29 | @font-family-sans-serif: Tahoma, Helvetica, Arial, sans-serif; 30 | @text-color: @jh-gray-darkest; 31 | @link-color: @jh-blue; 32 | @link-hover-color: @jh-blue-dark; 33 | 34 | @font-size-h1: 2.33em; 35 | @font-size-h2: 1.5em; 36 | @font-size-h3: 1.4em; 37 | @font-size-h4: 1em; 38 | 39 | // BUTTONS 40 | @btn-border-radius-base: 4px; 41 | 42 | @btn-primary-color: @jh-white; 43 | @btn-primary-bg: @jh-blue-light; 44 | @btn-primary-border: @jh-blue-light; 45 | // @btn-primary-bg: @jh-orange; 46 | // @btn-primary-border: @jh-orange-dark; 47 | 48 | @btn-default-color: @jh-gray-darkest; 49 | @btn-default-bg: #f1f1f1; 50 | @btn-default-border: #d2d2d2; 51 | 52 | @btn-success-bg: @jh-green-light; 53 | @btn-success-border: @jh-green; 54 | 55 | @btn-info-bg: @jh-blue-light; 56 | @btn-info-border: @jh-blue; 57 | 58 | @btn-warning-bg: @jh-orange; 59 | @btn-warning-border: @jh-orange-dark; 60 | 61 | @btn-danger-bg: @jh-red-light; 62 | @btn-danger-border: @jh-red; 63 | 64 | // TABS 65 | @nav-tabs-border-color: @jh-gray-light; 66 | 67 | @nav-tabs-link-hover-border-color: @jh-gray-dark; 68 | 69 | @nav-tabs-active-link-hover-bg: @jh-gray-light; 70 | @nav-tabs-active-link-hover-color: @jh-blue-dark; 71 | @nav-tabs-active-link-hover-border-color: @jh-gray-dark; 72 | 73 | @nav-tabs-justified-link-border-color: @jh-gray-light; 74 | @nav-tabs-justified-active-link-border-color: @jh-gray-dark; 75 | 76 | // WELLS 77 | @well-bg: #F1F4F7; 78 | @well-border: @jh-gray-light; 79 | 80 | // ALERTS 81 | @state-success-text: @jh-green; 82 | @state-success-bg: lighten(spin(@jh-green, -10), 65%); 83 | @state-success-border: @jh-green; 84 | 85 | @state-info-text: @jh-blue-dark; 86 | @state-info-bg: lighten(spin(@jh-blue, -10), 60%); 87 | @state-info-border: @jh-blue-dark; 88 | 89 | @state-warning-text: @jh-orange-dark; 90 | @state-warning-bg: lighten(spin(@jh-orange, -10), 40%); 91 | @state-warning-border: @jh-orange-dark; 92 | 93 | @state-danger-text: @jh-red; 94 | @state-danger-bg: lighten(spin(@jh-red, -10), 50%); 95 | @state-danger-border: @jh-red; 96 | 97 | // NAVBAR 98 | 99 | // Navbar Inverse 100 | @navbar-inverse-color: @jh-gray-lightest; 101 | @navbar-inverse-bg: @jh-blue-dark; 102 | @navbar-inverse-link-color: @jh-gray-lightest; 103 | @navbar-inverse-link-hover-color: @jh-gray-light; 104 | @navbar-inverse-link-disabled-color: @jh-gray-light; 105 | -------------------------------------------------------------------------------- /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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ], 19 | "types": [ 20 | "jasmine" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /website.publishproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | Debug 18 | AnyCPU 19 | 10.0.30319 20 | 2.0 21 | {8b1f18c4-89ce-44cd-8c94-3cce36470ddf} 22 | $(MSBuildThisFileDirectory) 23 | /PARTSNextClient 24 | v4.0 25 | 26 | 27 | 28 | 29 | 10.0 30 | 31 | 10.5 32 | $(VisualStudioVersion) 33 | 34 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(WebPublishTargetsVersion) 35 | <_WebPublishTargetsPath Condition=" '$(_WebPublishTargetsPath)'=='' ">$(VSToolsPath) 36 | 1.0.0.0 37 | 1.0.0.0 38 | 39 | 40 | 41 | $(AssemblyFileVersion) 42 | 43 | 44 | $(AssemblyVersion) 45 | 46 | 47 | 48 | --------------------------------------------------------------------------------