├── .editorconfig ├── .gitignore ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── karma.conf.js ├── mock_data ├── form │ ├── login.form.mock.json │ └── login.router.mock.json ├── menubar │ ├── menubar.admin.response.mock.json │ ├── menubar.anon.menuitems.mock.json │ └── menubar.anon.response.mock.json └── table │ ├── table.columns.response.mock.json │ ├── table.datatable.response.mock.json │ ├── table.records.response.mock.json │ └── table.selectitems.mock.json ├── package-lock.json ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── app.component.ts │ ├── app.module.ts │ ├── app.reducers.ts │ ├── app.routing.ts │ ├── auth │ │ ├── auth.actions.ts │ │ ├── auth.effects.ts │ │ ├── auth.models.ts │ │ ├── auth.module.ts │ │ ├── auth.reducers.ts │ │ └── auth.service.ts │ ├── form │ │ ├── form-element.component.ts │ │ ├── form.actions.ts │ │ ├── form.component.html │ │ ├── form.component.ts │ │ ├── form.container.spec.ts │ │ ├── form.container.ts │ │ ├── form.effects.ts │ │ ├── form.models.ts │ │ ├── form.module.ts │ │ ├── form.reducers.ts │ │ └── form.service.ts │ ├── growl │ │ ├── README.md │ │ ├── growl.component.ts │ │ └── growl.container.ts │ ├── home │ │ ├── home.container.html │ │ ├── home.container.scss │ │ └── home.container.ts │ ├── menubar │ │ ├── menubar.actions.ts │ │ ├── menubar.component.spec.ts │ │ ├── menubar.component.ts │ │ ├── menubar.constants.ts │ │ ├── menubar.container.spec.ts │ │ ├── menubar.container.ts │ │ ├── menubar.effects.ts │ │ ├── menubar.models.ts │ │ ├── menubar.module.ts │ │ ├── menubar.reducers.ts │ │ └── menubar.service.ts │ ├── rest │ │ ├── rest.actions.ts │ │ ├── rest.effects.ts │ │ ├── rest.models.ts │ │ ├── rest.reducers.ts │ │ └── rest.service.ts │ ├── router │ │ ├── router.actions.ts │ │ ├── router.effects.ts │ │ ├── router.models.ts │ │ └── router.serializer.ts │ ├── table │ │ ├── table.actions.ts │ │ ├── table.component.html │ │ ├── table.component.scss │ │ ├── table.component.spec.ts │ │ ├── table.component.ts │ │ ├── table.container.spec.ts │ │ ├── table.container.ts │ │ ├── table.data-mapping.component.html │ │ ├── table.data-mapping.component.ts │ │ ├── table.effects.ts │ │ ├── table.models.ts │ │ ├── table.module.ts │ │ ├── table.reducers.ts │ │ └── table.service.ts │ ├── util.ts │ └── websocket │ │ ├── websocket.actions.ts │ │ ├── websocket.effects.ts │ │ └── websocket.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── tsconfig.json ├── tslint.json └── uml ├── component_architecture.puml └── store_architecture.puml /.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 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # General Angular 2 | 3 | A web client for https://github.com/PierreRochard/general 4 | 5 | This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.28.3. 6 | 7 | ## Development server 8 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 9 | 10 | ## Code scaffolding 11 | 12 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 13 | 14 | ## Build 15 | 16 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 17 | 18 | ## Running unit tests 19 | 20 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 21 | 22 | ## Running end-to-end tests 23 | 24 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 25 | Before running the tests make sure you are serving the app via `ng serve`. 26 | 27 | ## Deploying to GitHub Pages 28 | 29 | Run `ng github-pages:deploy` to deploy to GitHub Pages. 30 | 31 | ## Further help 32 | 33 | To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 34 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { PrimengPostgrestPage } from './app.po'; 2 | 3 | describe('primeng-postgrest App', function() { 4 | let page: PrimengPostgrestPage; 5 | 6 | beforeEach(() => { 7 | page = new PrimengPostgrestPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class PrimengPostgrestPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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-remap-istanbul'), 12 | require('@angular-devkit/build-angular/plugins/karma') 13 | ], 14 | files: [ 15 | 16 | ], 17 | preprocessors: { 18 | 19 | }, 20 | mime: { 21 | 'text/x-typescript': ['ts','tsx'] 22 | }, 23 | remapIstanbulReporter: { 24 | dir: require('path').join(__dirname, 'coverage'), reports: { 25 | html: 'coverage', 26 | lcovonly: './coverage/coverage.lcov' 27 | } 28 | }, 29 | angularCli: { 30 | config: './.@angular-devkit/build-angular.json', 31 | environment: 'dev' 32 | }, 33 | reporters: config.angularCli && config.angularCli.codeCoverage 34 | ? ['progress', 'karma-remap-istanbul'] 35 | : ['progress'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['ChromeHeadless'], 41 | singleRun: false, 42 | customLaunchers: { 43 | ChromeHeadless: { 44 | base: 'Chrome', 45 | flags: [ 46 | '--headless', 47 | '--disable-gpu', 48 | // Without a remote debugging port, Google Chrome exits immediately. 49 | '--remote-debugging-port=9222', 50 | ], 51 | } 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /mock_data/form/login.form.mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaName": "auth", 3 | "formName": "login", 4 | "formSettings": { 5 | "id": 1, 6 | "custom_name": "Login", 7 | "form_name": "login", 8 | "schema_name": "auth", 9 | "user_id": "4bbed106-90a6-4371-91c9-c074f3cdf4bb" 10 | }, 11 | "fieldSettings": [ 12 | { 13 | "id": 1, 14 | "custom_name": "Email", 15 | "field_name": "email", 16 | "field_type": "text", 17 | "form_name": "login", 18 | "schema_name": "auth", 19 | "user_id": "4bbed106-90a6-4371-91c9-c074f3cdf4bb" 20 | }, 21 | { 22 | "id": 2, 23 | "custom_name": "Password", 24 | "field_name": "password", 25 | "field_type": "text", 26 | "form_name": "login", 27 | "schema_name": "auth", 28 | "user_id": "4bbed106-90a6-4371-91c9-c074f3cdf4bb" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /mock_data/form/login.router.mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": { 3 | "url": "/auth/rpc/login", 4 | "queryParams": {}, 5 | "params": { 6 | "selectedObjectName": "login", 7 | "selectedSchemaName": "auth", 8 | "selectedObjectType": "form" 9 | } 10 | }, 11 | "navigationId": 1 12 | } 13 | 14 | -------------------------------------------------------------------------------- /mock_data/menubar/menubar.admin.response.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Settings", 4 | "icon": "fa-cogs", 5 | "routerLink": null, 6 | "items": [ 7 | { 8 | "id": "2a2e1722-317c-42c3-bff2-3fd6675a6770", 9 | "icon": "fa-table", 10 | "user": "kXYfMlDqdaFReje", 11 | "items": null, 12 | "label": "Forms", 13 | "routerLink": [ 14 | "/", 15 | "admin", 16 | "forms" 17 | ], 18 | "submenu_id": "445c787a-6356-496f-a609-be58efc095cd", 19 | "order_index": 0 20 | }, 21 | { 22 | "id": "fcc3f3cb-3d43-4c9a-a978-d606a869503e", 23 | "icon": "fa-table", 24 | "user": "kXYfMlDqdaFReje", 25 | "items": null, 26 | "label": "Form Fields", 27 | "routerLink": [ 28 | "/", 29 | "admin", 30 | "form_fields" 31 | ], 32 | "submenu_id": "445c787a-6356-496f-a609-be58efc095cd", 33 | "order_index": 0 34 | }, 35 | { 36 | "id": "b59bb413-ad8c-4e05-9eab-aaaef6c9e9d0", 37 | "icon": "fa-table", 38 | "user": "kXYfMlDqdaFReje", 39 | "items": null, 40 | "label": "Datatables", 41 | "routerLink": [ 42 | "/", 43 | "admin", 44 | "datatables" 45 | ], 46 | "submenu_id": "445c787a-6356-496f-a609-be58efc095cd", 47 | "order_index": 0 48 | }, 49 | { 50 | "id": "2b7a6dc1-e4a0-490b-b9de-29a846b40580", 51 | "icon": "fa-table", 52 | "user": "kXYfMlDqdaFReje", 53 | "items": null, 54 | "label": "Datatable Columns", 55 | "routerLink": [ 56 | "/", 57 | "admin", 58 | "datatable_columns" 59 | ], 60 | "submenu_id": "445c787a-6356-496f-a609-be58efc095cd", 61 | "order_index": 0 62 | } 63 | ] 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /mock_data/menubar/menubar.anon.menuitems.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Login", 4 | "icon": "fa-pencil-square-o", 5 | "routerLink": [ 6 | "/", 7 | "auth", 8 | "rpc", 9 | "login" 10 | ] 11 | } 12 | ] 13 | 14 | -------------------------------------------------------------------------------- /mock_data/menubar/menubar.anon.response.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Login", 4 | "icon": "fa-pencil-square-o", 5 | "routerLink": [ 6 | "/", 7 | "auth", 8 | "rpc", 9 | "login" 10 | ], 11 | "items": null 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /mock_data/table/table.columns.response.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 60, 4 | "can_update": false, 5 | "column_name": "id", 6 | "custom_name": "Id", 7 | "data_type": "uuid", 8 | "filter_match_mode": "contains", 9 | "filter_value": null, 10 | "format_pattern": null, 11 | "input_type": "text", 12 | "is_filterable": false, 13 | "is_sortable": true, 14 | "is_visible": true, 15 | "schema_name": "test", 16 | "select_item_schema_name": null, 17 | "select_item_table_name": null, 18 | "select_item_label_column_name": null, 19 | "select_item_value_column_name": null, 20 | "table_name": "clients", 21 | "user_id": "1840e030-186e-4be3-a517-79762a96427d" 22 | }, 23 | { 24 | "id": 61, 25 | "can_update": true, 26 | "column_name": "household", 27 | "custom_name": "Household", 28 | "data_type": "character varying", 29 | "filter_match_mode": "contains", 30 | "filter_value": null, 31 | "format_pattern": null, 32 | "input_type": "dropdown", 33 | "is_filterable": false, 34 | "is_sortable": true, 35 | "is_visible": true, 36 | "schema_name": "test", 37 | "select_item_schema_name": "test", 38 | "select_item_table_name": "households", 39 | "select_item_label_column_name": "name", 40 | "select_item_value_column_name": "id", 41 | "table_name": "clients", 42 | "user_id": "1840e030-186e-4be3-a517-79762a96427d" 43 | }, 44 | { 45 | "id": 62, 46 | "can_update": false, 47 | "column_name": "first_name", 48 | "custom_name": "First Name", 49 | "data_type": "character varying", 50 | "filter_match_mode": "contains", 51 | "filter_value": null, 52 | "format_pattern": null, 53 | "input_type": "text", 54 | "is_filterable": false, 55 | "is_sortable": true, 56 | "is_visible": true, 57 | "schema_name": "test", 58 | "select_item_schema_name": null, 59 | "select_item_table_name": null, 60 | "select_item_label_column_name": null, 61 | "select_item_value_column_name": null, 62 | "table_name": "clients", 63 | "user_id": "1840e030-186e-4be3-a517-79762a96427d" 64 | }, 65 | { 66 | "id": 63, 67 | "can_update": false, 68 | "column_name": "last_name", 69 | "custom_name": "Last Name", 70 | "data_type": "character varying", 71 | "filter_match_mode": "contains", 72 | "filter_value": null, 73 | "format_pattern": null, 74 | "input_type": "text", 75 | "is_filterable": false, 76 | "is_sortable": true, 77 | "is_visible": true, 78 | "schema_name": "test", 79 | "select_item_schema_name": null, 80 | "select_item_table_name": null, 81 | "select_item_label_column_name": null, 82 | "select_item_value_column_name": null, 83 | "table_name": "clients", 84 | "user_id": "1840e030-186e-4be3-a517-79762a96427d" 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /mock_data/table/table.datatable.response.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 8, 4 | "custom_name": "Clients", 5 | "order_index": 1, 6 | "row_limit": 10, 7 | "row_offset": 0, 8 | "schema_name": "test", 9 | "sort_column": "id", 10 | "sort_order": 1, 11 | "table_name": "clients", 12 | "user_id": "1840e030-186e-4be3-a517-79762a96427d" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /mock_data/table/table.records.response.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "01e36eed-0509-4b6c-b1a9-d9f02d79c4ba", 4 | "household": null, 5 | "first_name": "PIERRE", 6 | "last_name": "ROCHARD" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /mock_data/table/table.selectitems.mock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "testing-value-1", 4 | "label": "Testing Value 1" 5 | }, 6 | { 7 | "value": "testing-value-2", 8 | "label": "Testing Value 2" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postgrest-angular", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "test": "ng test --colors=true --watch=true --progress=true --sourcemaps=false ", 10 | "pree2e": "webdriver-manager update --standalone false --gecko false", 11 | "e2e": "protractor" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^6.0.3", 16 | "@angular/cdk": "^6.1.0", 17 | "@angular/common": "^6.0.3", 18 | "@angular/compiler": "^6.0.3", 19 | "@angular/core": "^6.0.3", 20 | "@angular/forms": "^6.0.3", 21 | "@angular/http": "^6.0.3", 22 | "@angular/material": "^6.1.0", 23 | "@angular/platform-browser": "^6.0.3", 24 | "@angular/platform-browser-dynamic": "^6.0.3", 25 | "@angular/platform-server": "^6.0.3", 26 | "@angular/router": "^6.0.3", 27 | "@ngrx/core": "^1.2.0", 28 | "@ngrx/effects": "^6.0.1", 29 | "@ngrx/router-store": "^6.0.1", 30 | "@ngrx/store": "^6.0.1", 31 | "@ngrx/store-devtools": "^6.0.1", 32 | "@ngrx/store-log-monitor": "^3.0.2", 33 | "angular2-websocket": "^0.9.6", 34 | "core-js": "^2.5.6", 35 | "font-awesome": "^4.7.0", 36 | "ngrx-store-localstorage": "^5.0.0", 37 | "primeng": "^6.0.0-alpha.1", 38 | "raven-js": "^3.25.2", 39 | "reselect": "^3.0.1", 40 | "rxjs": "^6.2.0", 41 | "ts-helpers": "^1.1.2", 42 | "zone.js": "^0.8.26" 43 | }, 44 | "devDependencies": { 45 | "@angular-devkit/build-angular": "~0.6.5", 46 | "@angular/cli": "^6.0.5", 47 | "@angular/compiler-cli": "^6.0.3", 48 | "@types/jasmine": "2.8.7", 49 | "@types/node": "^10.1.2", 50 | "codelyzer": "^4.3.0", 51 | "jasmine-core": "3.1.0", 52 | "jasmine-spec-reporter": "4.2.1", 53 | "karma": "2.0.2", 54 | "karma-chrome-launcher": "^2.2.0", 55 | "karma-cli": "^1.0.1", 56 | "karma-jasmine": "^1.1.2", 57 | "karma-remap-istanbul": "^0.6.0", 58 | "ngrx-store-freeze": "^0.2.3", 59 | "protractor": "~5.3.2", 60 | "ts-node": "6.0.5", 61 | "tslint": "^5.10.0", 62 | "typescript": "<2.8" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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 | var 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 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: ` 6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | `, 14 | }) 15 | export class AppComponent { 16 | } 17 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { NgModule } from '@angular/core'; 5 | 6 | import { StoreModule } from '@ngrx/store'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | import { 9 | RouterStateSerializer, 10 | StoreRouterConnectingModule, 11 | } from '@ngrx/router-store'; 12 | 13 | import { FieldsetModule } from 'primeng/components/fieldset/fieldset'; 14 | import { GrowlModule } from 'primeng/components/growl/growl'; 15 | 16 | import { RestEffects } from './rest/rest.effects'; 17 | 18 | import { GrowlContainer } from './growl/growl.container'; 19 | import { GrowlComponent } from './growl/growl.component'; 20 | 21 | import { AppAuthModule } from './auth/auth.module'; 22 | import { AuthEffects } from './auth/auth.effects'; 23 | 24 | import { AppFormModule } from './form/form.module'; 25 | import { FormEffects } from './form/form.effects'; 26 | 27 | import { AppTableModule } from './table/table.module'; 28 | 29 | import { HomeContainer } from './home/home.container'; 30 | import { TableEffects } from './table/table.effects'; 31 | 32 | import { RestClient } from './rest/rest.service'; 33 | 34 | import { reducers, metaReducers } from './app.reducers'; 35 | import { routing } from './app.routing'; 36 | import { AppComponent } from './app.component'; 37 | 38 | import { WebsocketEffects } from './websocket/websocket.effects'; 39 | import { WebsocketService } from './websocket/websocket.service'; 40 | import { AppMenubarModule } from './menubar/menubar.module'; 41 | import { MenubarEffects } from './menubar/menubar.effects'; 42 | import { RouterEffects } from './router/router.effects'; 43 | import { CustomRouterStateSerializer } from './router/router.serializer'; 44 | 45 | @NgModule({ 46 | bootstrap: [AppComponent], 47 | declarations: [ 48 | AppComponent, 49 | HomeContainer, 50 | GrowlContainer, 51 | GrowlComponent, 52 | ], 53 | imports: [ 54 | AppAuthModule, 55 | AppFormModule, 56 | AppMenubarModule, 57 | AppTableModule, 58 | BrowserModule, 59 | BrowserAnimationsModule, 60 | EffectsModule.forRoot([AuthEffects, FormEffects, MenubarEffects, RestEffects, 61 | RouterEffects, TableEffects, WebsocketEffects]), 62 | FieldsetModule, 63 | GrowlModule, 64 | HttpClientModule, 65 | routing, 66 | StoreModule.forRoot(reducers, {metaReducers: metaReducers}), 67 | StoreRouterConnectingModule, 68 | ], 69 | providers: [ 70 | RestClient, 71 | WebsocketService, 72 | {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer}, 73 | ], 74 | }) 75 | export class AppModule { 76 | } 77 | -------------------------------------------------------------------------------- /src/app/app.reducers.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, ActionReducerMap, MetaReducer} from '@ngrx/store'; 2 | // import {storeFreeze} from 'ngrx-store-freeze'; 3 | import {LocalStorageConfig, localStorageSync} from 'ngrx-store-localstorage'; 4 | import {createSelector} from 'reselect'; 5 | 6 | import {environment} from '../environments/environment'; 7 | import {AuthState, authReducer} from './auth/auth.reducers'; 8 | import {FormState, formReducer} from './form/form.reducers'; 9 | import {MenubarState, menubarReducer} from './menubar/menubar.reducers'; 10 | import {RestState, restReducer} from './rest/rest.reducers'; 11 | import {TableState, tableReducer} from './table/table.reducers'; 12 | import {routerReducer, RouterReducerState} from '@ngrx/router-store'; 13 | import { RouterStateUrl } from './router/router.serializer'; 14 | 15 | export interface AppState { 16 | auth: AuthState; 17 | form: FormState, 18 | menubar: MenubarState; 19 | rest: RestState; 20 | routerReducer: RouterReducerState; 21 | table: TableState; 22 | } 23 | 24 | export const reducers: ActionReducerMap = { 25 | auth: authReducer, 26 | form: formReducer, 27 | menubar: menubarReducer, 28 | rest: restReducer, 29 | routerReducer: routerReducer, 30 | table: tableReducer, 31 | }; 32 | 33 | const localStorageConfig: LocalStorageConfig = { 34 | keys: ['auth'], 35 | rehydrate: true, 36 | storage: localStorage, 37 | removeOnUndefined: false, 38 | }; 39 | 40 | export function localStorageSyncReducer(reducer: ActionReducer): ActionReducer { 41 | return localStorageSync(localStorageConfig)(reducer); 42 | } 43 | 44 | export const metaReducers: MetaReducer[] = !environment.production 45 | ? [ 46 | // storeFreeze, 47 | localStorageSyncReducer 48 | ] 49 | : [ 50 | // storeFreeze, 51 | localStorageSyncReducer 52 | ]; 53 | 54 | 55 | // REST 56 | export const getRestState = (state: AppState) => state.rest; 57 | export const getResponse = (state: RestState) => state.response; 58 | export const getRestResponse = createSelector(getRestState, getResponse); 59 | 60 | 61 | export const getRouterState = (state: AppState) => state.routerReducer; 62 | export const getCurrentUrl = createSelector(getRouterState, (state: RouterReducerState) => state.state && state.state.url); 63 | export const getCurrentParams = createSelector(getRouterState, (state: RouterReducerState) => state.state && state.state.params); 64 | -------------------------------------------------------------------------------- /src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import {RouterModule, Routes} from '@angular/router'; 2 | 3 | import {FormContainer} from './form/form.container'; 4 | import {HomeContainer} from './home/home.container'; 5 | import {TableContainer} from './table/table.container'; 6 | 7 | export const routes: Routes = [ 8 | { 9 | component: HomeContainer, 10 | path: '', 11 | }, 12 | { 13 | component: FormContainer, 14 | path: ':selectedSchemaName/rpc/:selectedObjectName', 15 | }, 16 | { 17 | component: TableContainer, 18 | path: ':selectedSchemaName/:selectedObjectName', 19 | }, 20 | ]; 21 | 22 | export const routing = RouterModule.forRoot(routes, { useHash: false }); 23 | -------------------------------------------------------------------------------- /src/app/auth/auth.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { type } from '../util'; 4 | import { PostLoginRequestPayload } from './auth.models'; 5 | 6 | export const AuthActionTypes = { 7 | ADD_TOKEN: type('ADD_TOKEN'), 8 | REMOVE_TOKEN: type('REMOVE_TOKEN'), 9 | SEND_LOGIN_POST_REQUEST: type('SEND_LOGIN_POST_REQUEST'), 10 | }; 11 | 12 | export class SendLoginPostRequestAction implements Action { 13 | type = AuthActionTypes.SEND_LOGIN_POST_REQUEST; 14 | 15 | constructor(public payload: PostLoginRequestPayload) { 16 | } 17 | } 18 | 19 | export class AddTokenAction implements Action { 20 | type = AuthActionTypes.ADD_TOKEN; 21 | 22 | constructor(public payload: string) { 23 | } 24 | } 25 | 26 | export class RemoveTokenAction implements Action { 27 | type = AuthActionTypes.REMOVE_TOKEN; 28 | 29 | constructor(public payload: string) { 30 | } 31 | } 32 | 33 | export type AuthActions = AddTokenAction | RemoveTokenAction; 34 | -------------------------------------------------------------------------------- /src/app/auth/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { of } from 'rxjs/observable/of'; 4 | 5 | import { Actions, Effect } from '@ngrx/effects'; 6 | 7 | import { ReceivedResponseAction } from '../rest/rest.actions'; 8 | 9 | import { 10 | AddTokenAction, 11 | AuthActionTypes, 12 | SendLoginPostRequestAction, 13 | } from './auth.actions'; 14 | import { AuthService } from './auth.service'; 15 | import { GetMenubarAction } from '../menubar/menubar.actions'; 16 | import { Go } from '../router/router.actions'; 17 | import { PostLoginRequestPayload } from './auth.models'; 18 | 19 | 20 | @Injectable() 21 | export class AuthEffects { 22 | 23 | @Effect() 24 | sendPostRequest$ = this.actions$ 25 | .ofType(AuthActionTypes.SEND_LOGIN_POST_REQUEST) 26 | .map((action: SendLoginPostRequestAction) => action.payload) 27 | .switchMap((payload: PostLoginRequestPayload) => { 28 | return this.authService.post_login(payload.schemaName, payload.formName, payload.data) 29 | .map((response: any) => { 30 | const token = response.body[0]['token']; 31 | return new AddTokenAction(token); 32 | }) 33 | .catch(error => { 34 | return of(new ReceivedResponseAction(error)); 35 | }); 36 | }); 37 | 38 | @Effect() 39 | addToken$ = this.actions$ 40 | .ofType(AuthActionTypes.ADD_TOKEN) 41 | .switchMap(() => [ 42 | new Go({path: ['/']}), 43 | new GetMenubarAction()]); 44 | 45 | @Effect() 46 | removeToken$ = this.actions$ 47 | .ofType(AuthActionTypes.REMOVE_TOKEN) 48 | .switchMap(() => [ 49 | new Go({path: ['/']}), 50 | new GetMenubarAction()]); 51 | 52 | constructor(private actions$: Actions, 53 | private authService: AuthService, ) { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/auth/auth.models.ts: -------------------------------------------------------------------------------- 1 | export interface PostLoginRequestPayload { 2 | formName: string; 3 | schemaName: string; 4 | data: LoginData; 5 | } 6 | 7 | export interface LoginData { 8 | email: string; 9 | password: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | 5 | import { AuthService } from './auth.service'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | ], 11 | declarations: [ 12 | ], 13 | providers: [ 14 | AuthService, 15 | ], 16 | exports: [ 17 | ] 18 | }) 19 | export class AppAuthModule { } 20 | -------------------------------------------------------------------------------- /src/app/auth/auth.reducers.ts: -------------------------------------------------------------------------------- 1 | import {AuthActions, AuthActionTypes} from './auth.actions'; 2 | 3 | export interface AuthState { 4 | token: string | null; 5 | } 6 | 7 | const initialState: AuthState = { 8 | token: null, 9 | }; 10 | 11 | export function authReducer (state = initialState, action: AuthActions): AuthState { 12 | switch (action.type) { 13 | case AuthActionTypes.ADD_TOKEN: 14 | return Object.assign({}, state, { token: action.payload }); 15 | case AuthActionTypes.REMOVE_TOKEN: 16 | return Object.assign({}, state, { token: null}); 17 | default: 18 | return state; 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { RestClient } from 'app/rest/rest.service'; 6 | 7 | import { LoginData } from './auth.models'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | post_login(schemaName: string, formName: string, loginData: LoginData): Observable { 12 | const loginPath = '/rpc/' + formName; 13 | return this.restClient.post(schemaName, loginPath, loginData); 14 | }; 15 | 16 | constructor(private restClient: RestClient) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/form/form-element.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import {FormGroup} from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-dynamic-form-element', 6 | template: ` 7 |
8 |
9 | 12 |
13 |
14 | 19 | 20 | 26 |
27 |
28 | ` 29 | }) 30 | export class FormElementComponent { 31 | @Input() formFieldName: string; 32 | @Input() formFieldLabel: string; 33 | @Input() formGroup: FormGroup; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/form/form.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { RouteParams } from 'app/router/router.models'; 4 | 5 | import { Form, FormField } from './form.models'; 6 | 7 | 8 | export const GET_FORM_FIELD_SETTINGS = '[Form] Get Form Field Settings'; 9 | export const GET_FORM_SETTINGS = '[Form] Get Form Settings'; 10 | export const RECEIVE_FORM_FIELD_SETTINGS = '[Form] Receive Form Field Settings'; 11 | export const RECEIVE_FORM_SETTINGS = '[Form] Receive Form Settings'; 12 | export const SELECT_FORM = '[Form] Select Form'; 13 | 14 | export class GetFormFieldSettingsAction implements Action { 15 | readonly type = GET_FORM_FIELD_SETTINGS; 16 | 17 | constructor(public payload: RouteParams) { 18 | } 19 | } 20 | 21 | export class GetFormSettingsAction implements Action { 22 | readonly type = GET_FORM_SETTINGS; 23 | 24 | constructor(public payload: RouteParams) { 25 | } 26 | } 27 | 28 | export class ReceiveFormFieldSettingsAction implements Action { 29 | readonly type = RECEIVE_FORM_FIELD_SETTINGS; 30 | 31 | constructor(public payload: FormField[]) { 32 | } 33 | } 34 | 35 | export class ReceiveFormSettingsAction implements Action { 36 | readonly type = RECEIVE_FORM_SETTINGS; 37 | 38 | constructor(public payload: Form[]) { 39 | } 40 | } 41 | 42 | export class SelectFormAction implements Action { 43 | readonly type = SELECT_FORM; 44 | 45 | constructor(public payload: RouteParams) { 46 | } 47 | } 48 | 49 | export type FormActions = 50 | GetFormFieldSettingsAction 51 | | GetFormSettingsAction 52 | | ReceiveFormFieldSettingsAction 53 | | ReceiveFormSettingsAction 54 | | SelectFormAction; 55 | -------------------------------------------------------------------------------- /src/app/form/form.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |
7 | 8 |
9 |
10 | 16 | 17 |
18 |
19 |
20 |
21 |
22 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/app/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | Output, 7 | } from '@angular/core'; 8 | import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; 9 | 10 | import { FormField, Form } from './form.models'; 11 | 12 | 13 | @Component({ 14 | selector: 'app-form-component', 15 | templateUrl: './form.component.html', 16 | }) 17 | export class FormComponent implements OnChanges { 18 | @Input() formSettings: Form; 19 | @Input() formFieldSettings: FormField[]; 20 | @Output() onSubmit = new EventEmitter(); 21 | 22 | public form: FormGroup; 23 | 24 | constructor(private fb: FormBuilder) { 25 | } 26 | 27 | ngOnChanges(): void { 28 | const group: { [key: string]: any } = {}; 29 | this.formFieldSettings.map(settings => { 30 | group[settings.field_name] = new FormControl('') 31 | }); 32 | this.form = this.fb.group(group); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/form/form.container.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { 3 | async, 4 | ComponentFixture, 5 | TestBed, 6 | } from '@angular/core/testing'; 7 | import { RouterTestingModule } from '@angular/router/testing'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | 10 | import 'rxjs/add/operator/filter'; 11 | import 'rxjs/add/operator/map'; 12 | import 'rxjs/add/observable/of'; 13 | import 'rxjs/add/operator/takeUntil'; 14 | 15 | import { Store, StoreModule } from '@ngrx/store'; 16 | import { RouterReducerState } from '@ngrx/router-store'; 17 | 18 | import { AppState, reducers } from '../app.reducers'; 19 | import { RouterStateUrl } from 'app/router/router.serializer'; 20 | 21 | import { FormContainer } from './form.container'; 22 | import { FormService } from './form.service'; 23 | import { AppFormModule } from './form.module'; 24 | import { FormState } from './form.reducers'; 25 | import { SelectFormAction } from './form.actions'; 26 | 27 | const loginRouterMockData: RouterReducerState = require('../../../mock_data/form/login.router.mock.json'); 28 | const loginFormMockData: FormState = require('../../../mock_data/form/login.form.mock.json'); 29 | 30 | 31 | describe('Component: FormContainer', () => { 32 | 33 | let component: FormContainer; 34 | let fixture: ComponentFixture; 35 | let nativeElement: HTMLElement; 36 | let debugElement: DebugElement; 37 | 38 | let store: Store; 39 | 40 | beforeEach(async(() => { 41 | const testBed = TestBed.configureTestingModule({ 42 | declarations: [], 43 | imports: [ 44 | AppFormModule, 45 | StoreModule.forRoot(reducers, { 46 | initialState: { 47 | form: loginFormMockData, 48 | routerReducer: loginRouterMockData, 49 | }, 50 | }), 51 | RouterTestingModule, 52 | BrowserAnimationsModule, 53 | ], 54 | providers: [ 55 | FormService, 56 | ], 57 | }); 58 | testBed.compileComponents(); 59 | store = testBed.get(Store); 60 | })); 61 | 62 | beforeEach(() => { 63 | fixture = TestBed.createComponent(FormContainer); 64 | 65 | component = fixture.componentInstance; 66 | debugElement = fixture.debugElement; 67 | nativeElement = fixture.nativeElement; 68 | 69 | spyOn(store, 'dispatch').and.callThrough(); 70 | 71 | fixture.detectChanges(); 72 | }); 73 | describe('login form initialize', () => { 74 | it('will subscribe to formFieldSettings$', () => { 75 | component.formFieldSettings$.subscribe(data => { 76 | expect(data).toEqual(loginFormMockData.fieldSettings); 77 | }); 78 | }); 79 | 80 | it('will subscribe to formSettings$', () => { 81 | component.formSettings$.subscribe(data => { 82 | expect(data).toEqual(loginFormMockData.formSettings); 83 | }); 84 | }); 85 | 86 | it('will subscribe to selectedRouteParams$', () => { 87 | component.selectedRouteParams$.subscribe(data => { 88 | expect(data).toEqual(loginRouterMockData.state.params); 89 | }); 90 | }); 91 | it('will dispatch SelectFormAction', () => { 92 | expect(store.dispatch).toHaveBeenCalledWith(new SelectFormAction(loginRouterMockData.state.params)); 93 | }); 94 | }); 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /src/app/form/form.container.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core'; 2 | 3 | import { Action, Store } from '@ngrx/store'; 4 | 5 | import { Observable } from 'rxjs/Observable'; 6 | import { Subject } from 'rxjs/Subject'; 7 | 8 | import { SendPostRequestAction } from '../rest/rest.actions'; 9 | 10 | import { AppState, getCurrentParams } from '../app.reducers'; 11 | import { FormField } from './form.models'; 12 | import { SelectFormAction } from './form.actions' 13 | import { 14 | RemoveTokenAction, 15 | SendLoginPostRequestAction, 16 | } from '../auth/auth.actions'; 17 | import { Go } from '../router/router.actions'; 18 | import { RouteParams } from '../router/router.models'; 19 | 20 | 21 | @Component({ 22 | selector: 'app-form-container', 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | template: ` 25 | 30 | `, 31 | }) 32 | export class FormContainer implements OnDestroy, OnInit { 33 | public formFieldSettings$: Observable; 34 | public formSettings$: Observable; 35 | public selectedRouteParams$: Observable; 36 | 37 | private ngUnsubscribe: Subject = new Subject(); 38 | 39 | constructor(private store: Store) { 40 | } 41 | 42 | ngOnInit() { 43 | 44 | this.formFieldSettings$ = this.store.select(state => state.form.fieldSettings); 45 | this.formSettings$ = this.store.select(state => state.form.formSettings); 46 | this.selectedRouteParams$ = this.store.select(getCurrentParams); 47 | 48 | this.selectedRouteParams$ 49 | .filter(selectedRouteParams => selectedRouteParams.selectedObjectType === 'form') 50 | .takeUntil(this.ngUnsubscribe).subscribe(selectedRouteParams => { 51 | if (selectedRouteParams.selectedObjectName === 'logout') { 52 | this.store.dispatch(new RemoveTokenAction('')); 53 | this.store.dispatch(new Go({path: ['/']})); 54 | } else { 55 | this.store.dispatch(new SelectFormAction(selectedRouteParams)); 56 | } 57 | }) 58 | } 59 | 60 | public onSubmit(formValue: any) { 61 | Object.keys(formValue).filter(key => formValue[key] === '') 62 | .map(key => delete formValue[key]); 63 | this.selectedRouteParams$.take(1).subscribe(selectedRouteParams => { 64 | let postAction: Action; 65 | const post = { 66 | schemaName: selectedRouteParams.selectedSchemaName, 67 | formName: selectedRouteParams.selectedObjectName, 68 | data: formValue, 69 | }; 70 | if (post.formName === 'login') { 71 | postAction = new SendLoginPostRequestAction(post); 72 | this.store.dispatch(postAction) 73 | } else { 74 | postAction = new SendPostRequestAction(post); 75 | this.store.dispatch(postAction) 76 | } 77 | }, 78 | ); 79 | } 80 | 81 | ngOnDestroy() { 82 | this.ngUnsubscribe.next(); 83 | this.ngUnsubscribe.complete(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/form/form.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect } from '@ngrx/effects'; 4 | 5 | import { of } from 'rxjs/observable/of'; 6 | 7 | import { 8 | ReceiveFormFieldSettingsAction, 9 | GetFormFieldSettingsAction, GetFormSettingsAction, ReceiveFormSettingsAction, 10 | GET_FORM_FIELD_SETTINGS, GET_FORM_SETTINGS, SELECT_FORM, SelectFormAction, 11 | } from './form.actions'; 12 | import { FormService } from './form.service'; 13 | import { RouteParams } from '../router/router.models'; 14 | 15 | 16 | @Injectable() 17 | export class FormEffects { 18 | 19 | @Effect() 20 | selectForm$ = this.actions$ 21 | .ofType(SELECT_FORM) 22 | .map((action: SelectFormAction) => action.payload) 23 | .mergeMap((routeParams: RouteParams) => { 24 | return [ 25 | new GetFormSettingsAction(routeParams), 26 | new GetFormFieldSettingsAction(routeParams), 27 | ]; 28 | }); 29 | 30 | @Effect() 31 | getFormSettings$ = this.actions$ 32 | .ofType(GET_FORM_SETTINGS) 33 | .switchMap((action: GetFormSettingsAction) => { 34 | return this.formService.get_form_settings(action.payload.selectedSchemaName, 35 | action.payload.selectedObjectName) 36 | .mergeMap((response: any) => { 37 | return [ 38 | new ReceiveFormSettingsAction(response.body), 39 | ]; 40 | }) 41 | .catch(error => { 42 | return of(new ReceiveFormSettingsAction(error)); 43 | }) 44 | }); 45 | 46 | @Effect() 47 | getFormFieldSettings$ = this.actions$ 48 | .ofType(GET_FORM_FIELD_SETTINGS) 49 | .switchMap((action: GetFormFieldSettingsAction) => { 50 | return this.formService.get_form_field_settings(action.payload.selectedSchemaName, 51 | action.payload.selectedObjectName) 52 | .mergeMap((response: any) => { 53 | return [ 54 | new ReceiveFormFieldSettingsAction(response.body), 55 | ]; 56 | }) 57 | .catch(error => { 58 | return of(new ReceiveFormFieldSettingsAction(error)); 59 | }) 60 | }); 61 | 62 | 63 | constructor(private actions$: Actions, 64 | private formService: FormService) { 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/form/form.models.ts: -------------------------------------------------------------------------------- 1 | export interface Form { 2 | custom_name: string; 3 | form_name: string; 4 | schema_name: string; 5 | user_id: string; 6 | } 7 | 8 | export interface FormField { 9 | custom_name: string; 10 | field_name: string; 11 | field_type: string; 12 | form_name: string; 13 | schema_name: string; 14 | user_id: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/form/form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { ButtonModule } from 'primeng/components/button/button'; 6 | import { FieldsetModule } from 'primeng/components/fieldset/fieldset'; 7 | import { InputTextModule } from 'primeng/components/inputtext/inputtext'; 8 | import { PasswordModule } from 'primeng/components/password/password'; 9 | 10 | import { FormContainer } from './form.container'; 11 | import { FormComponent } from './form.component'; 12 | import { FormElementComponent } from './form-element.component'; 13 | import { FormService } from './form.service'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | ButtonModule, 18 | CommonModule, 19 | FieldsetModule, 20 | InputTextModule, 21 | PasswordModule, 22 | ReactiveFormsModule, 23 | ], 24 | declarations: [ 25 | FormComponent, 26 | FormContainer, 27 | FormElementComponent, 28 | ], 29 | providers: [ 30 | FormService, 31 | ], 32 | exports: [ 33 | FormContainer, 34 | ] 35 | }) 36 | export class AppFormModule { } 37 | -------------------------------------------------------------------------------- /src/app/form/form.reducers.ts: -------------------------------------------------------------------------------- 1 | import { Form, FormField } from 'app/form/form.models'; 2 | 3 | import { 4 | FormActions, 5 | RECEIVE_FORM_FIELD_SETTINGS, 6 | RECEIVE_FORM_SETTINGS, 7 | SELECT_FORM, 8 | } from './form.actions'; 9 | 10 | export interface FormState { 11 | schemaName: string | null; 12 | formName: string | null; 13 | formSettings: Form | null; 14 | fieldSettings: FormField[]; 15 | } 16 | 17 | const initialState: FormState = { 18 | schemaName: null, 19 | formName: null, 20 | formSettings: null, 21 | fieldSettings: [], 22 | }; 23 | 24 | export function formReducer(state = initialState, action: FormActions): FormState { 25 | switch (action.type) { 26 | case SELECT_FORM: 27 | return Object.assign({}, state, { 28 | schemaName: action.payload.selectedSchemaName, 29 | formName: action.payload.selectedObjectName, 30 | }); 31 | case RECEIVE_FORM_FIELD_SETTINGS: 32 | return Object.assign({}, state, { 33 | fieldSettings: action.payload, 34 | }); 35 | case RECEIVE_FORM_SETTINGS: 36 | return Object.assign({}, state, { 37 | formSettings: action.payload[0], 38 | }); 39 | default: 40 | return state; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/form/form.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import { HttpParams } from '@angular/common/http'; 3 | 4 | import {Observable} from 'rxjs/Observable'; 5 | 6 | import {RestClient} from 'app/rest/rest.service'; 7 | 8 | @Injectable() 9 | export class FormService { 10 | get_form_field_settings(schemaName: string, formName: string): Observable { 11 | const params: HttpParams = new HttpParams(); 12 | params.set('form_name', 'eq.' + formName); 13 | params.set('schema_name', 'eq.' + schemaName); 14 | return this.restClient.get('admin', '/form_fields', params); 15 | }; 16 | get_form_settings(schemaName: string, formName: string): Observable { 17 | const params: HttpParams = new HttpParams(); 18 | params.set('form_name', 'eq.' + formName); 19 | params.set('schema_name', 'eq.' + schemaName); 20 | return this.restClient.get('admin', '/forms', params); 21 | }; 22 | constructor(private restClient: RestClient) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/app/growl/README.md: -------------------------------------------------------------------------------- 1 | Lot of work to do on the growl: 2 | 1. Create actions 3 | 2. Create a store, reducer 4 | 3. Effects? 5 | 4. Implement actions in other services 6 | 7 | -------------------------------------------------------------------------------- /src/app/growl/growl.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | import {Message} from 'primeng/primeng'; 4 | 5 | @Component({ 6 | selector: 'app-growl-component', 7 | template: ``, 10 | }) 11 | export class GrowlComponent { 12 | @Input() messages: Message[]; 13 | 14 | sticky = false; 15 | life = 500; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/growl/growl.container.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Response} from '@angular/http'; 3 | 4 | import {Observable} from 'rxjs/Observable'; 5 | 6 | import {Store} from '@ngrx/store'; 7 | 8 | import {Message} from 'primeng/primeng'; 9 | 10 | import {AppState, getRestResponse} from '../app.reducers'; 11 | 12 | 13 | @Component({ 14 | selector: 'app-growl-container', 15 | template: ` 17 | `, 18 | }) 19 | export class GrowlContainer { 20 | messages$: Observable; 21 | 22 | constructor(private store: Store) { 23 | this.messages$ = store.select(getRestResponse) 24 | .filter(response => response !== null) 25 | .map((response: any) => { 26 | let severity = 'info'; 27 | let summary = ''; 28 | let detail = ''; 29 | let response_message = ''; 30 | if (response) { 31 | response_message = response.message; 32 | } 33 | if (response.status === 0) { 34 | summary = 'Unable to connect to the API'; 35 | severity = 'error'; 36 | } else if (response.status >= 200 && response.status < 300) { 37 | summary = response.status.toString() + ': ' + response.statusText; 38 | detail = response_message; 39 | severity = 'success'; 40 | } else if (response.status >= 400 && response.status < 600) { 41 | summary = response.status.toString() + ': ' + response.statusText; 42 | detail = response_message; 43 | severity = 'error'; 44 | } 45 | return [{severity: severity, 46 | summary: summary, 47 | detail: detail}]; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/home/home.container.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Welcome! 6 |

7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /src/app/home/home.container.scss: -------------------------------------------------------------------------------- 1 | .home-title { 2 | font-family: "Roboto", "Trebuchet MS", Arial, Helvetica, sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/home/home.container.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home-container', 5 | templateUrl: './home.container.html', 6 | styleUrls: ['./home.container.scss'] 7 | }) 8 | export class HomeContainer { 9 | } 10 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { GeneralMenuItem } from 'app/menubar/menubar.models'; 4 | 5 | export const GET_MENUBAR = '[Menubar] Get Menubar'; 6 | export const RECEIVE_MENUBAR = '[Menubar] Receive Menubar'; 7 | 8 | export class GetMenubarAction implements Action { 9 | readonly type = GET_MENUBAR; 10 | } 11 | 12 | export class ReceiveMenubarAction implements Action { 13 | readonly type = RECEIVE_MENUBAR; 14 | 15 | constructor(public payload: GeneralMenuItem[]) { 16 | } 17 | } 18 | 19 | export type MenubarActions = GetMenubarAction | ReceiveMenubarAction; 20 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { 4 | async, 5 | ComponentFixture, 6 | TestBed, 7 | } from '@angular/core/testing'; 8 | import { By } from '@angular/platform-browser'; 9 | import { RouterTestingModule } from '@angular/router/testing'; 10 | 11 | import 'rxjs/add/operator/map'; 12 | 13 | import { MenubarModule } from 'primeng/components/menubar/menubar'; 14 | 15 | import { menubarLoadingState } from './menubar.constants'; 16 | import { MenubarComponent } from './menubar.component'; 17 | 18 | const menubarAnonMenuitemsMockData = require('../../../mock_data/menubar/menubar.anon.menuitems.mock.json'); 19 | 20 | 21 | describe('Component: MenubarComponent', () => { 22 | 23 | let component: MenubarComponent; 24 | let fixture: ComponentFixture; 25 | let nativeElement: HTMLElement; 26 | let testNativeElement: HTMLElement; 27 | let debugElement: DebugElement; 28 | 29 | let getDebugElement: Function; 30 | let getNativeElement: Function; 31 | 32 | beforeEach(async(() => { 33 | const testBed = TestBed.configureTestingModule({ 34 | declarations: [ 35 | MenubarComponent, 36 | ], 37 | imports: [ 38 | CommonModule, 39 | MenubarModule, 40 | RouterTestingModule 41 | ], 42 | providers: [], 43 | }); 44 | testBed.compileComponents(); 45 | })); 46 | 47 | beforeEach(() => { 48 | fixture = TestBed.createComponent(MenubarComponent); 49 | 50 | component = fixture.componentInstance; 51 | debugElement = fixture.debugElement; 52 | nativeElement = fixture.nativeElement; 53 | 54 | getDebugElement = (selector: string): DebugElement => { 55 | return debugElement.query(By.css(selector)); 56 | }; 57 | 58 | getNativeElement = (selector: string): HTMLElement => { 59 | return getDebugElement(selector).nativeElement; 60 | }; 61 | 62 | fixture.detectChanges(); 63 | }); 64 | 65 | it('should render PrimeNG menubar component', () => { 66 | testNativeElement = getNativeElement('p-menubar'); 67 | expect(testNativeElement).toBeDefined(); 68 | }); 69 | 70 | describe(' unloaded ', () => { 71 | 72 | beforeEach(() => { 73 | component.items = menubarLoadingState; 74 | fixture.detectChanges(); 75 | }); 76 | 77 | it('should render Loading menuitem', () => { 78 | testNativeElement = getNativeElement('.ui-menuitem-text'); 79 | expect(testNativeElement.textContent).toEqual(menubarLoadingState[0].label); 80 | }); 81 | 82 | }); 83 | 84 | describe(' for anon ', () => { 85 | 86 | beforeEach(() => { 87 | component.items = menubarAnonMenuitemsMockData; 88 | fixture.detectChanges(); 89 | }); 90 | 91 | it('should render Login menuitem', () => { 92 | testNativeElement = getNativeElement('.ui-menuitem-text'); 93 | expect(testNativeElement.textContent).toEqual(menubarAnonMenuitemsMockData[0].label); 94 | }); 95 | }) 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { MenuItem } from 'primeng/components/common/menuitem'; 4 | 5 | 6 | @Component({ 7 | selector: 'app-menubar-component', 8 | template: ` 9 |
10 |
11 | 13 | 14 |
15 |
16 | `, 17 | }) 18 | export class MenubarComponent { 19 | @Input() items: MenuItem[] = []; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.constants.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem } from 'primeng/primeng'; 2 | 3 | export const menubarLoadingState: MenuItem[] = [{'label': 'Loading...', 'disabled': true}]; 4 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.container.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { 4 | async, 5 | ComponentFixture, 6 | TestBed, 7 | } from '@angular/core/testing'; 8 | 9 | import 'rxjs/add/operator/map'; 10 | 11 | import { Store, StoreModule } from '@ngrx/store'; 12 | 13 | import { MenubarModule } from 'primeng/components/menubar/menubar'; 14 | 15 | import { AppState, metaReducers, reducers } from '../app.reducers'; 16 | 17 | import { GetMenubarAction, ReceiveMenubarAction } from './menubar.actions'; 18 | import { MenubarContainer } from './menubar.container'; 19 | import { menubarLoadingState } from './menubar.constants'; 20 | import { MenubarComponent } from './menubar.component'; 21 | 22 | const menubarAnonResponseMockData = require('../../../mock_data/menubar/menubar.anon.response.mock.json'); 23 | const menubarAnonMenuitemsMockData = require('../../../mock_data/menubar/menubar.anon.menuitems.mock.json'); 24 | 25 | 26 | describe('Component: MenubarContainer', () => { 27 | 28 | let component: MenubarContainer; 29 | let fixture: ComponentFixture; 30 | let nativeElement: HTMLElement; 31 | let debugElement: DebugElement; 32 | 33 | let store: Store; 34 | let action; 35 | 36 | beforeEach(async(() => { 37 | const testBed = TestBed.configureTestingModule({ 38 | declarations: [ 39 | MenubarContainer, 40 | MenubarComponent, 41 | ], 42 | imports: [ 43 | CommonModule, 44 | MenubarModule, 45 | StoreModule.forRoot(reducers, {metaReducers: metaReducers}), 46 | ], 47 | providers: [], 48 | }); 49 | testBed.compileComponents(); 50 | store = testBed.get(Store); 51 | spyOn(store, 'dispatch').and.callThrough(); 52 | })); 53 | 54 | beforeEach(() => { 55 | fixture = TestBed.createComponent(MenubarContainer); 56 | 57 | component = fixture.componentInstance; 58 | debugElement = fixture.debugElement; 59 | nativeElement = fixture.nativeElement; 60 | 61 | fixture.detectChanges(); 62 | }); 63 | 64 | describe(' unloaded ', () => { 65 | 66 | it('should subscribe to items$', () => { 67 | component.items$.subscribe(data => { 68 | expect(data).toEqual(menubarLoadingState); 69 | }); 70 | }); 71 | 72 | it('should dispatch a GetMenubarAction', () => { 73 | expect(store.dispatch).toHaveBeenCalledWith(new GetMenubarAction()); 74 | }); 75 | 76 | }); 77 | 78 | describe(' for anon ', () => { 79 | 80 | beforeEach(() => { 81 | action = new ReceiveMenubarAction(menubarAnonResponseMockData); 82 | store.dispatch(action); 83 | }); 84 | 85 | it('should subscribe to items$', () => { 86 | component.items$.subscribe(data => { 87 | expect(data).toEqual(menubarAnonMenuitemsMockData); 88 | }); 89 | }); 90 | }) 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.container.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Store } from '@ngrx/store'; 6 | 7 | import { MenuItem } from 'primeng/primeng'; 8 | 9 | import { AppState } from '../app.reducers'; 10 | import { GetMenubarAction } from './menubar.actions'; 11 | import { menubarLoadingState } from './menubar.constants'; 12 | 13 | 14 | @Component({ 15 | selector: 'app-menubar-container', 16 | template: ` 17 | 20 | 21 | `, 22 | }) 23 | export class MenubarContainer implements OnInit { 24 | items$: Observable; 25 | 26 | constructor(private store: Store) { 27 | } 28 | 29 | ngOnInit() { 30 | this.items$ = this.store.select(state => state.menubar.menuItems) 31 | .map(menuItems => { 32 | if (menuItems === null) { 33 | this.store.dispatch(new GetMenubarAction()); 34 | return menubarLoadingState; 35 | } 36 | return menuItems; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect } from '@ngrx/effects'; 4 | 5 | import { of } from 'rxjs/observable/of'; 6 | 7 | import { GET_MENUBAR, ReceiveMenubarAction } from './menubar.actions'; 8 | import { MenubarService } from './menubar.service'; 9 | import { RemoveTokenAction } from '../auth/auth.actions'; 10 | 11 | 12 | @Injectable() 13 | export class MenubarEffects { 14 | 15 | @Effect() 16 | sendGetRequest$ = this.actions$ 17 | .ofType(GET_MENUBAR) 18 | .switchMap(() => this.menubarService.get() 19 | .mergeMap((response: any) => { 20 | return [ 21 | new ReceiveMenubarAction(response.body), 22 | ]; 23 | }) 24 | .catch(error => { 25 | const unauthorizedCode = 401; 26 | if (error.status === unauthorizedCode) { 27 | return of(new RemoveTokenAction('')) 28 | } 29 | return of(new ReceiveMenubarAction(error)); 30 | }) 31 | ); 32 | 33 | constructor(private actions$: Actions, 34 | private menubarService: MenubarService,) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.models.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem } from 'primeng/primeng'; 2 | 3 | export interface GeneralMenuItem extends MenuItem { 4 | [key: string]: { [k: string]: any; } | string | MenuItem[] | MenuItem[][] | boolean | undefined | ((event?: any) => void); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { MenubarModule } from 'primeng/primeng'; 5 | 6 | import { MenubarContainer } from './menubar.container'; 7 | import { MenubarService } from './menubar.service'; 8 | import { MenubarComponent } from './menubar.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | MenubarModule, 14 | ], 15 | declarations: [ 16 | MenubarComponent, 17 | MenubarContainer, 18 | ], 19 | providers: [ 20 | MenubarService, 21 | ], 22 | exports: [ 23 | MenubarContainer, 24 | ] 25 | }) 26 | export class AppMenubarModule { } 27 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.reducers.ts: -------------------------------------------------------------------------------- 1 | import { MenubarActions, RECEIVE_MENUBAR } from './menubar.actions'; 2 | import { GeneralMenuItem } from './menubar.models'; 3 | 4 | export interface MenubarState { 5 | menuItems: GeneralMenuItem[] | null; 6 | } 7 | 8 | const initialState: MenubarState = { 9 | menuItems: null, 10 | }; 11 | 12 | export function menubarReducer(state = initialState, action: MenubarActions): MenubarState { 13 | switch (action.type) { 14 | case RECEIVE_MENUBAR: 15 | console.log(action); 16 | return Object.assign({}, state, { 17 | menuItems: action.payload.map((item: GeneralMenuItem) => { 18 | return Object.keys(item).reduce((result: GeneralMenuItem, key) => { 19 | // Removing keys that have a falsy value to avoid undesirable 20 | // menubar behavior 21 | if (item[key]) { 22 | result[key] = item[key]; 23 | } 24 | return result; 25 | }, {}) 26 | }) 27 | }); 28 | default: 29 | return state; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/menubar/menubar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { RestClient } from 'app/rest/rest.service'; 6 | 7 | @Injectable() 8 | export class MenubarService { 9 | get(): Observable { 10 | return this.restClient.get('admin', '/menubar'); 11 | }; 12 | 13 | constructor(private restClient: RestClient) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/rest/rest.actions.ts: -------------------------------------------------------------------------------- 1 | import {Response} from '@angular/http'; 2 | 3 | import {Action} from '@ngrx/store'; 4 | 5 | import {type} from '../util'; 6 | 7 | export const RestActionTypes = { 8 | SEND_GET_REQUEST: type('SEND_GET_REQUEST'), 9 | SEND_POST_REQUEST: type('SEND_POST_REQUEST'), 10 | SEND_DELETE_REQUEST: type('SEND_DELETE_REQUEST'), 11 | RECEIVED_RESPONSE: type('RECEIVED_RESPONSE'), 12 | RECEIVED_SCHEMA: type('RECEIVED_SCHEMA'), 13 | }; 14 | 15 | export class SendGetRequestAction implements Action { 16 | type = RestActionTypes.SEND_GET_REQUEST; 17 | constructor(public payload: any) {} 18 | } 19 | export class SendPostRequestAction implements Action { 20 | type = RestActionTypes.SEND_POST_REQUEST; 21 | constructor(public payload: any) {} 22 | } 23 | export class SendDeleteRequestAction implements Action { 24 | type = RestActionTypes.SEND_DELETE_REQUEST; 25 | constructor(public payload: any) {} 26 | } 27 | export class ReceivedResponseAction implements Action { 28 | type = RestActionTypes.RECEIVED_RESPONSE; 29 | constructor(public payload: any) {} 30 | } 31 | 32 | export type RestActions 33 | = SendGetRequestAction 34 | | SendPostRequestAction 35 | | SendDeleteRequestAction 36 | | ReceivedResponseAction; 37 | -------------------------------------------------------------------------------- /src/app/rest/rest.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Action } from '@ngrx/store'; 5 | import { of } from 'rxjs'; 6 | import { map, switchMap } from 'rxjs/operators'; 7 | 8 | import { 9 | AddTokenAction, RemoveTokenAction, 10 | } from '../auth/auth.actions'; 11 | import { 12 | ReceivedResponseAction, RestActionTypes, 13 | SendGetRequestAction, SendPostRequestAction, 14 | } from './rest.actions'; 15 | import { RestClient } from './rest.service'; 16 | 17 | import { Go } from '../router/router.actions'; 18 | 19 | @Injectable() 20 | export class RestEffects { 21 | 22 | @Effect() 23 | sendGetRequest$ = this.actions$.pipe( 24 | ofType(RestActionTypes.SEND_GET_REQUEST), 25 | switchMap((action: SendGetRequestAction) => this.http 26 | .get(action.payload.schema, 27 | action.payload.path) 28 | .mergeMap((response: any) => { 29 | return [ 30 | new ReceivedResponseAction(response), 31 | ]; 32 | }) 33 | .catch((error: any) => { 34 | return of(new ReceivedResponseAction(error)); 35 | }), 36 | )); 37 | 38 | @Effect() 39 | sendPostRequest$ = this.actions$.pipe( 40 | ofType(RestActionTypes.SEND_POST_REQUEST), 41 | switchMap((action: SendPostRequestAction) => { 42 | return this.http.post(action.payload.schema, 43 | action.payload.path, 44 | action.payload.data) 45 | .map((response: any) => { 46 | return new ReceivedResponseAction(response); 47 | }) 48 | .catch((error: any) => { 49 | return of(new ReceivedResponseAction(error)); 50 | }); 51 | })); 52 | 53 | // Fix route store selector getCurrentUrl 54 | // @Effect() 55 | // sendDeleteRequest$ = this.actions$ 56 | // .ofType(RestActionTypes.SEND_DELETE_REQUEST) 57 | // .map((action: SendDeleteRequestAction) => action) 58 | // .withLatestFrom(this.store) 59 | // .switchMap(([action, store]) => { 60 | // console.log(store.router.state.root); 61 | // return this.http.delete(store.router.state.root, action.payload) 62 | // .map(response => { 63 | // return new ReceivedResponseAction(response); 64 | // }) 65 | // .catch(error => { 66 | // return of(new ReceivedResponseAction(error)); 67 | // }); 68 | // }); 69 | 70 | @Effect() 71 | processResponse$ = this.actions$.pipe( 72 | ofType(RestActionTypes.RECEIVED_RESPONSE), 73 | map((action: ReceivedResponseAction) => action), 74 | switchMap((action): Action[] => { 75 | let response_url: any; 76 | let response_data: any | Promise; 77 | const response: any = action.payload; 78 | switch (response.status) { 79 | case 200: 80 | response_data = action.payload.body; 81 | response_url = action.payload.url; 82 | if (response_url === 'https://api.rochard.org/rpc/login') { 83 | return [new AddTokenAction(response_data[0].token)]; 84 | } 85 | return []; 86 | case 204: 87 | return []; 88 | case 401: 89 | if (response.message === 'JWT expired') { 90 | return [ 91 | new RemoveTokenAction(''), 92 | new Go({path: ['/rpc/login']}), 93 | ]; 94 | } else { 95 | return []; 96 | } 97 | default: 98 | return []; 99 | } 100 | })); 101 | 102 | constructor(private actions$: Actions, 103 | private http: RestClient, ) { 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/app/rest/rest.models.ts: -------------------------------------------------------------------------------- 1 | export interface PostFormRequestActionPayload { 2 | schemaName: string; 3 | formName: string; 4 | data?: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/rest/rest.reducers.ts: -------------------------------------------------------------------------------- 1 | import {Response} from '@angular/http'; 2 | 3 | import {RestActions, RestActionTypes} from './rest.actions'; 4 | 5 | 6 | export interface RestState { 7 | posting: boolean; 8 | receivedForm: boolean; 9 | getting: boolean; 10 | response: Response | null; 11 | } 12 | 13 | const initialState: RestState = { 14 | posting: false, 15 | receivedForm: false, 16 | getting: false, 17 | response: null 18 | }; 19 | 20 | export function restReducer (state = initialState, action: RestActions): RestState { 21 | switch (action.type) { 22 | case RestActionTypes.SEND_GET_REQUEST: 23 | return Object.assign({}, state, { 24 | getting: true, 25 | received: false, 26 | }); 27 | case RestActionTypes.SEND_POST_REQUEST: 28 | return Object.assign({}, state, { 29 | posting: true, 30 | received: false, 31 | }); 32 | case RestActionTypes.RECEIVED_RESPONSE: 33 | return Object.assign({}, state, { 34 | posting: false, 35 | received: true, 36 | response: action.payload, 37 | }); 38 | default: 39 | return state; 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/app/rest/rest.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpClient, 4 | HttpHeaders, 5 | HttpParams, 6 | HttpResponse, 7 | } from '@angular/common/http'; 8 | 9 | import { Observable } from 'rxjs/Observable'; 10 | 11 | import { Store } from '@ngrx/store'; 12 | 13 | import { AppState } from '../app.reducers'; 14 | 15 | @Injectable() 16 | export class RestClient { 17 | 18 | private _timeout = 3000; 19 | 20 | static createEndpoint(schemaName: string, endpoint: string) { 21 | const _domain = '.rochard.org'; 22 | const _schemaName = schemaName.replace(/_/g, ''); 23 | return 'https://' + _schemaName + _domain + endpoint 24 | } 25 | 26 | static createAuthorizationHeader(headers: HttpHeaders, token: string | null) { 27 | if (!!token) { 28 | headers = headers.set('Authorization', 'Bearer ' + token); 29 | } 30 | return headers; 31 | } 32 | 33 | get(schemaName: string, endpoint: string, params?: HttpParams): Observable { 34 | return this.store.take(1).switchMap(state => { 35 | let headers = RestClient.createAuthorizationHeader(new HttpHeaders(), state.auth.token); 36 | headers = headers.set('prefer', 'return=representation'); 37 | headers = headers.set('prefer', 'count=exact'); 38 | return this.http.get(RestClient.createEndpoint(schemaName, endpoint), 39 | { 40 | headers: headers, 41 | observe: 'response', 42 | params: params, 43 | }).timeout(this._timeout); 44 | }, 45 | ); 46 | }; 47 | 48 | post(schemaName: string, endpoint: string, data: any): Observable { 49 | return this.store.take(1).switchMap(state => { 50 | let headers = RestClient.createAuthorizationHeader(new HttpHeaders(), state.auth.token); 51 | headers = headers.set('prefer', 'return=representation'); 52 | return this.http.post(RestClient.createEndpoint(schemaName, endpoint), data, 53 | { 54 | headers: headers, 55 | observe: 'response', 56 | }).timeout(this._timeout); 57 | }, 58 | ); 59 | }; 60 | 61 | delete(schemaName: string, endpoint: string, id: string): Observable { 62 | return this.store.take(1).switchMap(state => { 63 | const params: HttpParams = new HttpParams(); 64 | let headers = RestClient.createAuthorizationHeader(new HttpHeaders(), state.auth.token); 65 | headers = headers.set('prefer', 'return=minimal'); 66 | params.set('id', 'eq.' + id); 67 | return this.http.delete(RestClient.createEndpoint(schemaName, endpoint), { 68 | headers: headers, 69 | observe: 'response', 70 | params: params, 71 | }).timeout(this._timeout); 72 | }); 73 | } 74 | 75 | patch(schemaName: string, endpoint: string, data: any, params: HttpParams): Observable { 76 | return this.store.take(1).switchMap(state => { 77 | let headers = RestClient.createAuthorizationHeader(new HttpHeaders(), state.auth.token); 78 | headers = headers.set('prefer', 'return=representation'); 79 | return this.http.patch(RestClient.createEndpoint(schemaName, endpoint), data, 80 | { 81 | headers: headers, 82 | observe: 'response', 83 | params: params, 84 | }).timeout(this._timeout); 85 | }); 86 | } 87 | 88 | constructor(private http: HttpClient, 89 | private store: Store) { 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/app/router/router.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { NavigationExtras } from '@angular/router'; 3 | 4 | export const GO = '[Router] Go'; 5 | export const BACK = '[Router] Back'; 6 | export const FORWARD = '[Router] Forward'; 7 | 8 | export class Go implements Action { 9 | readonly type = GO; 10 | 11 | constructor(public payload: { 12 | path: any[]; 13 | query?: object; 14 | extras?: NavigationExtras; 15 | }) { 16 | } 17 | } 18 | 19 | export class Back implements Action { 20 | readonly type = BACK; 21 | } 22 | 23 | export class Forward implements Action { 24 | readonly type = FORWARD; 25 | } 26 | 27 | export type Actions 28 | = Go 29 | | Back 30 | | Forward; 31 | -------------------------------------------------------------------------------- /src/app/router/router.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | 5 | import { Effect, Actions } from '@ngrx/effects'; 6 | 7 | import { BACK, FORWARD, GO, Go } from './router.actions'; 8 | 9 | 10 | @Injectable() 11 | export class RouterEffects { 12 | @Effect({dispatch: false}) 13 | navigate$ = this.actions$.ofType(GO) 14 | .map((action: Go) => action.payload) 15 | .do(({path, query: queryParams, extras}) => { 16 | return this.router.navigate(path, {queryParams, ...extras}) 17 | }); 18 | 19 | @Effect({dispatch: false}) 20 | navigateBack$ = this.actions$.ofType(BACK) 21 | .do(() => this.location.back()); 22 | 23 | @Effect({dispatch: false}) 24 | navigateForward$ = this.actions$.ofType(FORWARD) 25 | .do(() => this.location.forward()); 26 | 27 | constructor(private actions$: Actions, 28 | private router: Router, 29 | private location: Location) { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/router/router.models.ts: -------------------------------------------------------------------------------- 1 | export interface RouteParams { 2 | selectedSchemaName: string; 3 | selectedObjectName: string; 4 | selectedObjectType: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/router/router.serializer.ts: -------------------------------------------------------------------------------- 1 | import { Params, RouterStateSnapshot } from '@angular/router'; 2 | 3 | import { RouterStateSerializer } from '@ngrx/router-store'; 4 | 5 | import { RouteParams } from './router.models'; 6 | 7 | export interface RouterStateUrl { 8 | url: string; 9 | queryParams: Params; 10 | params: RouteParams; 11 | } 12 | 13 | export class CustomRouterStateSerializer implements RouterStateSerializer { 14 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 15 | const {url} = routerState; 16 | const queryParams = routerState.root.queryParams; 17 | const params: any = {}; 18 | if (Object.keys(routerState.root.children[0].params).length > 0) { 19 | params.selectedObjectName = routerState.root.children[0].params.selectedObjectName; 20 | params.selectedSchemaName = routerState.root.children[0].params.selectedSchemaName; 21 | if (routerState.root.children[0].url.length > 0) { 22 | params.selectedObjectType = routerState.root.children[0].url[1].path === 'rpc' 23 | ? 'form' 24 | : 'table'; 25 | } 26 | } else { 27 | params.selectedSchemaName = null; 28 | params.selectedObjectName = null; 29 | params.selectedObjectType = 'home'; 30 | } 31 | // Only return an object including the URL and query params 32 | // instead of the entire snapshot 33 | return {url, queryParams, params}; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/table/table.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { RouteParams } from '../router/router.models'; 4 | 5 | import { 6 | Datatable, DatatableColumn, UpdateRecord, 7 | SuggestionsQuery, DeleteRecord, 8 | } from './table.models'; 9 | 10 | export const ADD_RECORD = '[Table] Add Record'; 11 | export const ARE_RECORDS_LOADING = '[Table] Are Records Loading'; 12 | export const DELETE_RECORD = '[Table] Delete Record'; 13 | export const DESELECT_RECORD = '[Table] Deselect Record'; 14 | export const DESELECT_RECORDS = '[Table] Deselect Records'; 15 | export const EDIT_CANCEL = '[Table] Edit Cancel'; 16 | export const GET_DATATABLE = '[Table] Get Datatable'; 17 | export const GET_DATATABLE_COLUMNS = '[Table] Get Datatable Columns'; 18 | export const GET_RECORDS = '[Table] Get Records'; 19 | export const GET_SUGGESTIONS = '[Table] Get Select Items'; 20 | export const INITIALIZE_SETTINGS = '[Table] Initialize Settings'; 21 | export const RECEIVE_DATATABLE = '[Table] Receive Datatable'; 22 | export const RECEIVE_DATATABLE_COLUMNS = '[Table] Receive Datatable Columns'; 23 | export const RECEIVE_RECORDS = '[Table] Receive Records'; 24 | export const RECEIVE_SUGGESTIONS = '[Table] Receive Select Items'; 25 | export const REMOVE_RECORD = '[Table] Remove Record'; 26 | export const REMOVE_RECORDS = '[Table] Remove Records'; 27 | export const SELECT_RECORDS = '[Table] Select Records'; 28 | export const SELECT_TABLE = '[Table] Select Table'; 29 | export const UPDATE_COLUMNS_VISIBILITY = '[Table] Update Columns Visibility'; 30 | export const UPDATE_KEYWORD= '[Table] Update Keyword'; 31 | export const UPDATE_PAGINATION = '[Table] Update Pagination'; 32 | export const UPDATE_RECORD = '[Table] Update Record'; 33 | export const UPDATE_ROW_COUNT = '[Table] Update Row Count'; 34 | export const UPDATE_SORT = '[Table] Update Sort'; 35 | export const UPDATE_TABLE_NAME = '[Table] Update Table Name'; 36 | 37 | export class AddRecordAction implements Action { 38 | type = ADD_RECORD; 39 | constructor(public payload: any) {} 40 | } 41 | 42 | export class AreRecordsLoadingAction implements Action { 43 | type = ARE_RECORDS_LOADING; 44 | constructor(public payload: any) {} 45 | } 46 | 47 | export class DeleteRecordAction implements Action { 48 | type = DELETE_RECORD; 49 | constructor(public payload: DeleteRecord) {} 50 | } 51 | 52 | export class DeselectRecordAction implements Action { 53 | type = DESELECT_RECORD; 54 | constructor(public payload: any) {} 55 | } 56 | 57 | export class DeselectRecordsAction implements Action { 58 | type = DESELECT_RECORDS; 59 | constructor(public payload: any) {} 60 | } 61 | 62 | export class EditCancelAction implements Action { 63 | type = EDIT_CANCEL; 64 | constructor(public payload: any) {} 65 | } 66 | 67 | export class GetDatatableAction implements Action { 68 | type = GET_DATATABLE; 69 | constructor(public payload: RouteParams) { } 70 | } 71 | 72 | export class GetDatatableColumnsAction implements Action { 73 | type = GET_DATATABLE_COLUMNS; 74 | constructor(public payload: RouteParams) { } 75 | } 76 | 77 | export class GetRecordsAction implements Action { 78 | type = GET_RECORDS; 79 | constructor(public payload: Datatable) { } 80 | } 81 | 82 | export class GetSuggestionsAction implements Action { 83 | type = GET_SUGGESTIONS; 84 | constructor(public payload: SuggestionsQuery) { } 85 | } 86 | 87 | export class InitializeSettingsAction implements Action { 88 | type = INITIALIZE_SETTINGS; 89 | constructor(public payload: any) {} 90 | } 91 | 92 | export class ReceiveDatatableAction implements Action { 93 | type = RECEIVE_DATATABLE; 94 | constructor(public payload: Datatable) {} 95 | } 96 | 97 | export class ReceiveDatatableColumnsAction implements Action { 98 | type = RECEIVE_DATATABLE_COLUMNS; 99 | constructor(public payload: DatatableColumn[]) {} 100 | } 101 | 102 | export class ReceiveRecordsAction implements Action { 103 | type = RECEIVE_RECORDS; 104 | constructor(public payload: any[]) {} 105 | } 106 | 107 | export class ReceiveSuggestionsAction implements Action { 108 | type = RECEIVE_SUGGESTIONS; 109 | constructor(public payload: any[]) {} 110 | } 111 | 112 | export class RemoveRecordAction implements Action { 113 | type = REMOVE_RECORD; 114 | constructor(public payload: any) {} 115 | } 116 | 117 | export class RemoveTableRecordsAction implements Action { 118 | type = REMOVE_RECORDS; 119 | constructor(public payload: string) { } 120 | } 121 | 122 | export class SelectRecordsAction implements Action { 123 | type = SELECT_RECORDS; 124 | constructor(public payload: any) {} 125 | } 126 | 127 | export class SelectTableAction implements Action { 128 | type = SELECT_TABLE; 129 | constructor(public payload: RouteParams) {} 130 | } 131 | 132 | export class UpdateColumnsVisibilityAction implements Action { 133 | type = UPDATE_COLUMNS_VISIBILITY; 134 | constructor(public payload: any) {} 135 | } 136 | 137 | export class UpdateKeywordAction implements Action { 138 | type = UPDATE_KEYWORD; 139 | constructor(public payload: any) {} 140 | } 141 | 142 | export class UpdatePaginationAction implements Action { 143 | type = UPDATE_PAGINATION; 144 | constructor(public payload: any) {} 145 | } 146 | 147 | export class UpdateRecordAction implements Action { 148 | type = UPDATE_RECORD; 149 | constructor(public payload: UpdateRecord) {} 150 | } 151 | 152 | export class UpdateRowCountAction implements Action { 153 | type = UPDATE_ROW_COUNT; 154 | constructor(public payload: number) {} 155 | } 156 | 157 | export class UpdateSortAction implements Action { 158 | type = UPDATE_SORT; 159 | constructor(public payload: any) {} 160 | } 161 | 162 | export class UpdateTableNameAction implements Action { 163 | type = UPDATE_TABLE_NAME; 164 | constructor(public payload: RouteParams) {} 165 | } 166 | 167 | 168 | export type TableActions 169 | = AddRecordAction 170 | | AreRecordsLoadingAction 171 | | DeleteRecordAction 172 | | DeselectRecordAction 173 | | DeselectRecordsAction 174 | | GetDatatableColumnsAction 175 | | GetRecordsAction 176 | | InitializeSettingsAction 177 | | ReceiveDatatableColumnsAction 178 | | ReceiveRecordsAction 179 | | RemoveRecordAction 180 | | RemoveTableRecordsAction 181 | | SelectRecordsAction 182 | | UpdateColumnsVisibilityAction 183 | | UpdatePaginationAction 184 | | UpdateRecordAction 185 | | UpdateRowCountAction 186 | | UpdateSortAction 187 | | UpdateTableNameAction; 188 | -------------------------------------------------------------------------------- /src/app/table/table.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 27 | 28 |
29 | {{datatable.custom_name}} 30 |
31 |
32 | 36 | 37 | 46 | 47 | 48 | 59 | 60 |
61 | 62 | 63 | {{row[column.column_name] | date:column.format_pattern}} 64 | 65 | 66 | 67 | {{row[column.column_name] | number:column.format_pattern}} 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {{item[column.select_item_label_column_name]}} 76 | 77 | 78 | 79 | {{row[column.column_name][column.select_item_label_column_name]}} 80 | 81 | 82 | 83 | 84 | {{row[column.column_name] | json}} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {{row[column.column_name]}} 100 | 101 |
102 |
103 | 104 | 108 | 109 | 116 | 126 | 127 | {{tmpSelectedItem[column.select_item_label_column_name]}} tmpSelectedItem 128 | 129 | 130 | {{tmpItem[column.select_item_label_column_name]}} tmpItem 131 | 132 | 133 | 134 | 135 |
136 |
137 |
138 |
139 | -------------------------------------------------------------------------------- /src/app/table/table.component.scss: -------------------------------------------------------------------------------- 1 | .general-column { 2 | overflow-x: visible; 3 | overflow-y: visible; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/app/table/table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { 4 | async, 5 | ComponentFixture, 6 | TestBed, 7 | } from '@angular/core/testing'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { By } from '@angular/platform-browser'; 10 | import { RouterTestingModule } from '@angular/router/testing'; 11 | 12 | import 'rxjs/add/operator/map'; 13 | 14 | import { DataTableModule } from 'primeng/components/datatable/datatable'; 15 | import { DropdownModule } from 'primeng/components/dropdown/dropdown'; 16 | 17 | import { TableComponent } from './table.component'; 18 | import { Datatable, DatatableColumn } from './table.models'; 19 | import { SelectItem } from 'primeng/primeng'; 20 | 21 | const columnsMockData: DatatableColumn[] = require('../../../mock_data/table/table.columns.response.mock.json'); 22 | const datatableMockDatas: Datatable[] = require('../../../mock_data/table/table.datatable.response.mock.json'); 23 | const recordsMockData: any[] = require('../../../mock_data/table/table.records.response.mock.json'); 24 | const selectItemsMockData: SelectItem[] = require('../../../mock_data/table/table.selectitems.mock.json'); 25 | 26 | const datatableMockData: Datatable = datatableMockDatas[0]; 27 | 28 | describe('Component: TableComponent', () => { 29 | 30 | let component: TableComponent; 31 | let fixture: ComponentFixture; 32 | let nativeElement: HTMLElement; 33 | let debugElement: DebugElement; 34 | 35 | let testDebugElement: DebugElement; 36 | let testNativeElement: HTMLElement; 37 | 38 | let subTestDebugElement: DebugElement; 39 | let subTestNativeElement: HTMLElement; 40 | 41 | let getDebugElement: Function; 42 | let getNativeElement: Function; 43 | 44 | beforeEach(async(() => { 45 | const testBed = TestBed.configureTestingModule({ 46 | declarations: [ 47 | TableComponent, 48 | ], 49 | imports: [ 50 | BrowserAnimationsModule, 51 | CommonModule, 52 | DataTableModule, 53 | DropdownModule, 54 | RouterTestingModule, 55 | ], 56 | providers: [], 57 | }); 58 | testBed.compileComponents(); 59 | })); 60 | 61 | beforeEach(() => { 62 | fixture = TestBed.createComponent(TableComponent); 63 | 64 | component = fixture.componentInstance; 65 | debugElement = fixture.debugElement; 66 | nativeElement = fixture.nativeElement; 67 | 68 | getDebugElement = (selector: string): DebugElement => { 69 | return debugElement.query(By.css(selector)); 70 | }; 71 | 72 | getNativeElement = (selector: string): HTMLElement => { 73 | return getDebugElement(selector).nativeElement; 74 | }; 75 | 76 | fixture.detectChanges(); 77 | }); 78 | 79 | it('should render PrimeNG datatable component', () => { 80 | testNativeElement = getNativeElement('p-datatable'); 81 | expect(testNativeElement).toBeDefined(); 82 | }); 83 | 84 | describe(' loaded ', () => { 85 | 86 | beforeEach(() => { 87 | component.areRecordsLoading = true; 88 | component.columns = columnsMockData; 89 | component.datatable = datatableMockData; 90 | component.records = recordsMockData; 91 | component.totalRecords = recordsMockData.length; 92 | spyOn(component.getSuggestions, 'emit').and.callThrough(); 93 | spyOn(component.onEditComplete, 'emit').and.callThrough(); 94 | fixture.detectChanges(); 95 | }); 96 | 97 | describe('dropdown editing', () => { 98 | beforeEach(() => { 99 | testNativeElement = getNativeElement('.ui-dropdown'); 100 | testNativeElement.click(); 101 | fixture.detectChanges(); 102 | }); 103 | 104 | it('should emit to getSuggestions when clicked', () => { 105 | expect(component.getSuggestions.emit).toHaveBeenCalled(); 106 | }); 107 | 108 | it('should render dropdown search', () => { 109 | expect(getNativeElement('.ui-dropdown-filter')).toBeDefined(); 110 | }); 111 | 112 | it('should render dropdown label', () => { 113 | expect(getNativeElement('.ui-dropdown-item').textContent.trim()) 114 | .toEqual('Testing Value 1'); 115 | }); 116 | 117 | it('should emit to onEditComplete when clicked', () => { 118 | subTestNativeElement = getNativeElement('.ui-dropdown-item'); 119 | subTestNativeElement.click(); 120 | fixture.detectChanges(); 121 | expect(component.onEditComplete.emit).toHaveBeenCalledWith( 122 | { 123 | 'column': columnsMockData[1], 124 | 'row': recordsMockData[0], 125 | 'value': selectItemsMockData[0].value, 126 | }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/app/table/table.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation, 3 | } from '@angular/core'; 4 | 5 | import { 6 | Datatable, DatatableColumn, DatatableUpdate, EditEvent, 7 | MultiselectOutput, DeleteRecord, UpdateRecord, SuggestionsQuery, 8 | } from 'app/table/table.models'; 9 | import { Column, DataTable } from 'primeng/primeng'; 10 | 11 | @Component({ 12 | selector: 'app-table-component', 13 | templateUrl: './table.component.html', 14 | styleUrls: ['./table.component.scss'], 15 | encapsulation: ViewEncapsulation.None, 16 | }) 17 | export class TableComponent { 18 | // todo: move these into table settings 19 | public dataKey = 'id'; 20 | public editable = true; 21 | public lazy = true; 22 | public paginator = true; 23 | public reorderableColumns = false; 24 | public resizableColumns = true; 25 | public responsive = true; 26 | public rowHover = true; 27 | public rowsPerPage = [10, 20]; 28 | public scrollable = true; 29 | public style = {'overflow': 'visible', 'margin': 'auto'}; 30 | 31 | @Input() areRecordsLoading: boolean; 32 | @Input() columns: DatatableColumn[]; 33 | @Input() datatable: Datatable; 34 | _records: any[]; 35 | @Input() 36 | set records(value: any[]) { 37 | console.log(value); 38 | this._records = JSON.parse(JSON.stringify(value)); 39 | }; 40 | 41 | get records() { 42 | return this._records; 43 | } 44 | 45 | @Input() suggestions: string[]; 46 | @Input() totalRecords: number; 47 | 48 | @Output() getSuggestions = new EventEmitter(); 49 | @Output() onDelete = new EventEmitter(); 50 | @Output() onEditCancel = new EventEmitter(); 51 | @Output() onEditComplete = new EventEmitter(); 52 | @Output() onFilterAdded = new EventEmitter(); 53 | @Output() onFilterRemoved = new EventEmitter(); 54 | @Output() onPagination = new EventEmitter(); 55 | @Output() onSort = new EventEmitter(); 56 | @Output() onMultiselect = new EventEmitter(); 57 | 58 | @ViewChild('dt') dt: DataTable; 59 | 60 | get actionColumn(): any { 61 | if (this.datatable.can_archive) { 62 | return { 63 | is_visible: true, 64 | styles: { 65 | 'height': '38px', 66 | 'overflow': 'visible', 67 | 'padding-top': '0px', 68 | 'padding-bottom': '0px', 69 | 'width': '120px', 70 | 'text-align': 'center', 71 | }, 72 | } 73 | } else { 74 | return []; 75 | } 76 | } 77 | 78 | archiveRow(event: MouseEvent, row: any) { 79 | console.log(event); 80 | console.log(row); 81 | const recordDelete: DeleteRecord = { 82 | record_id: row['id'], 83 | table_name: this.datatable.table_name, 84 | schema_name: this.datatable.schema_name, 85 | }; 86 | this.onDelete.emit(recordDelete); 87 | } 88 | 89 | get datatableWidth(): string { 90 | const columnWidths = this.columns.concat(this.actionColumn).filter(c => { 91 | return c.is_visible; 92 | }) 93 | .map(c => Number(c.styles.width.slice(0, -2))); 94 | let totalColumnWidths = columnWidths.reduce(function (sum, value): number { 95 | return sum + value; 96 | }, 0.0); 97 | totalColumnWidths += 2; 98 | totalColumnWidths = Math.max(totalColumnWidths, 400); 99 | totalColumnWidths = Math.min(totalColumnWidths, 1100); 100 | return totalColumnWidths.toString() + 'px' 101 | } 102 | 103 | onCellEditorKeydown(event: any, column: Column, rowData: any, rowIndex: number) { 104 | this.dt.onEdit.emit({ 105 | originalEvent: event, 106 | column: column, 107 | data: rowData, 108 | index: rowIndex, 109 | }); 110 | 111 | if (event.keyCode === 13) { // enter 112 | this.dt.onEditComplete.emit({ 113 | column: column, 114 | data: rowData, 115 | index: rowIndex, 116 | }); 117 | if (event.shiftKey) { 118 | this.dt.moveToPreviousCell(event); 119 | } else { 120 | this.dt.moveToNextCell(event); 121 | } 122 | } else if (event.keyCode === 27) { // escape 123 | this.dt.onEditCancel.emit({ 124 | column: column, 125 | data: rowData, 126 | index: rowIndex, 127 | }); 128 | this.dt.domHandler.invokeElementMethod(event.target, 'blur'); 129 | this.dt.switchCellToViewMode(event.target); 130 | event.preventDefault(); 131 | } else if (event.keyCode === 9) { // tab 132 | this.dt.onEditComplete.emit({ 133 | column: column, 134 | data: rowData, 135 | index: rowIndex, 136 | }); 137 | if (event.shiftKey) { 138 | this.dt.moveToPreviousCell(event); 139 | } else { 140 | this.dt.moveToNextCell(event); 141 | } 142 | } 143 | } 144 | 145 | onLazyLoad(event: DatatableUpdate): void { 146 | event.tableName = this.datatable.table_name; 147 | event.schemaName = this.datatable.schema_name; 148 | if (event.first !== this.datatable.row_offset || event.rows !== this.datatable.row_limit) { 149 | this.onPagination.emit(event); 150 | } 151 | if (event.sortOrder !== this.datatable.sort_order || event.sortField !== this.datatable.sort_column) { 152 | this.onSort.emit(event); 153 | } 154 | } 155 | 156 | updateRecord(event: EditEvent) { 157 | const update: UpdateRecord = { 158 | value: event.data[event.column.field], 159 | record_id: event.data['id'], 160 | column_name: event.column.field, 161 | table_name: this.datatable.table_name, 162 | schema_name: this.datatable.schema_name, 163 | }; 164 | this.onEditComplete.emit(update); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/app/table/table.container.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { 4 | async, 5 | ComponentFixture, 6 | TestBed, 7 | } from '@angular/core/testing'; 8 | 9 | import 'rxjs/add/operator/map'; 10 | import { Observable } from 'rxjs/Observable'; 11 | 12 | import { Store, StoreModule } from '@ngrx/store'; 13 | 14 | 15 | import { AppState, metaReducers, reducers } from '../app.reducers'; 16 | 17 | import { Datatable, DatatableColumn } from './table.models'; 18 | import { TableComponent } from 'app/table/table.component'; 19 | import { TableContainer } from './table.container'; 20 | import { RouteParams } from 'app/router/router.models'; 21 | 22 | const columnsMockData: DatatableColumn[] = require('../../../mock_data/table/table.columns.response.mock.json'); 23 | const datatableMockDatas: Datatable[] = require('../../../mock_data/table/table.datatable.response.mock.json'); 24 | const recordsMockData: any[] = require('../../../mock_data/table/table.records.response.mock.json'); 25 | 26 | export class DataStub { 27 | public static get(): Observable { 28 | return Observable.of({ 29 | 'selectedObjectName': 'login', 30 | 'selectedSchemaName': 'auth', 31 | 'selectedObjectType': 'form', 32 | }); 33 | } 34 | } 35 | 36 | describe('Component: TableContainer', () => { 37 | 38 | let component: TableContainer; 39 | let fixture: ComponentFixture; 40 | let nativeElement: HTMLElement; 41 | let debugElement: DebugElement; 42 | 43 | let store: Store; 44 | let action; 45 | 46 | beforeEach(async(() => { 47 | const testBed = TestBed.configureTestingModule({ 48 | declarations: [ 49 | TableContainer, 50 | TableComponent, 51 | ], 52 | imports: [ 53 | CommonModule, 54 | StoreModule.forRoot(reducers, {metaReducers: metaReducers}), 55 | ], 56 | providers: [], 57 | }); 58 | testBed.compileComponents(); 59 | store = testBed.get(Store); 60 | spyOn(store, 'dispatch').and.callThrough(); 61 | })); 62 | 63 | beforeEach(() => { 64 | fixture = TestBed.createComponent(TableContainer); 65 | 66 | component = fixture.componentInstance; 67 | debugElement = fixture.debugElement; 68 | nativeElement = fixture.nativeElement; 69 | 70 | fixture.detectChanges(); 71 | }); 72 | 73 | describe(' initialize ', () => { 74 | 75 | }); 76 | 77 | describe(' for anon ', () => { 78 | 79 | // beforeEach(() => { 80 | // action = new ReceiveMenubarAction(menubarAnonResponseMockData); 81 | // store.dispatch(action); 82 | // }); 83 | // 84 | // it('should subscribe to items$', () => { 85 | // component.items$.subscribe(value => { 86 | // expect(value).toEqual(menubarAnonMenuitemsMockData); 87 | // }); 88 | // }); 89 | }) 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /src/app/table/table.container.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, Component, OnDestroy, 3 | OnInit, 4 | } from '@angular/core'; 5 | 6 | import { Observable } from 'rxjs/Observable'; 7 | import { Subject } from 'rxjs/Subject'; 8 | 9 | import { Store } from '@ngrx/store'; 10 | 11 | import { LazyLoadEvent } from 'primeng/primeng'; 12 | 13 | import { AppState, getCurrentParams } from '../app.reducers'; 14 | import { RouteParams } from '../router/router.models'; 15 | 16 | import { 17 | DeleteRecordAction, 18 | GetSuggestionsAction, 19 | UpdateRecordAction, 20 | UpdateColumnsVisibilityAction, 21 | UpdateKeywordAction, 22 | UpdatePaginationAction, 23 | UpdateSortAction, 24 | SelectTableAction, 25 | } from './table.actions'; 26 | import { 27 | Datatable, DatatableColumn, EditEvent, 28 | MultiselectOutput, DeleteRecord, UpdateRecord, SuggestionsQuery, 29 | } from './table.models'; 30 | 31 | @Component({ 32 | changeDetection: ChangeDetectionStrategy.OnPush, 33 | selector: 'app-table-container', 34 | template: ` 35 | 41 | 42 | 58 | `, 59 | }) 60 | export class TableContainer implements OnDestroy, OnInit { 61 | public areColumnsLoading$: Observable; 62 | public areRecordsLoading$: Observable; 63 | public columns$: Observable; 64 | public datatable$: Observable; 65 | public isDatatableLoading$: Observable; 66 | public records$: Observable; 67 | public selectedRouteParams$: Observable; 68 | public suggestions$: Observable; 69 | public totalRecords$: Observable; 70 | 71 | private ngUnsubscribe: Subject = new Subject(); 72 | 73 | constructor(private store: Store) { 74 | } 75 | 76 | ngOnInit() { 77 | this.areColumnsLoading$ = this.store.select(state => state.table.areColumnsLoading); 78 | this.areRecordsLoading$ = this.store.select(state => state.table.areRecordsLoading); 79 | this.columns$ = this.store.select(state => state.table.columns); 80 | this.datatable$ = this.store.select(state => state.table.datatable); 81 | this.isDatatableLoading$ = this.store.select(state => state.table.isDatatableLoading); 82 | this.records$ = this.store.select(state => state.table.records); 83 | this.selectedRouteParams$ = this.store.select(getCurrentParams); 84 | this.suggestions$ = this.store.select(state => state.table.suggestions); 85 | this.totalRecords$ = this.store.select(state => state.table.rowCount); 86 | 87 | this.selectedRouteParams$ 88 | .filter(selectedRouteParams => selectedRouteParams.selectedObjectType === 'table') 89 | .takeUntil(this.ngUnsubscribe) 90 | .subscribe(selectedRouteParams => { 91 | this.store.dispatch(new SelectTableAction(selectedRouteParams)); 92 | }); 93 | } 94 | 95 | getSuggestions(query: SuggestionsQuery) { 96 | this.store.dispatch(new GetSuggestionsAction(query)); 97 | } 98 | 99 | getKeywordSuggestions(query: SuggestionsQuery) { 100 | this.store.dispatch(new UpdateKeywordAction(query)); 101 | this.store.dispatch(new GetSuggestionsAction(query)); 102 | } 103 | 104 | onDelete(deleteRecord: DeleteRecord) { 105 | this.store.dispatch(new DeleteRecordAction(deleteRecord)); 106 | } 107 | 108 | onEditCancel(event: EditEvent) { 109 | // const routeParams: RouteParams = { 110 | // selectedObjectName: event.table_name, 111 | // selectedSchemaName: event.schema_name, 112 | // selectedObjectType: 'table', 113 | // }; 114 | // this.store.dispatch(new GetDatatableAction(routeParams)); 115 | } 116 | 117 | onEditComplete(event: UpdateRecord) { 118 | this.store.dispatch(new UpdateRecordAction(event)); 119 | } 120 | 121 | onPagination(event: LazyLoadEvent) { 122 | this.store.dispatch(new UpdatePaginationAction(event)); 123 | } 124 | 125 | onSort(event: LazyLoadEvent) { 126 | this.store.dispatch(new UpdateSortAction(event)); 127 | } 128 | 129 | updateColumns(event: MultiselectOutput) { 130 | if (event.added.length > 0) { 131 | this.store.dispatch(new UpdateColumnsVisibilityAction({ 132 | columns: event.added, 133 | tableName: event.tableName, 134 | isVisible: true, 135 | })); 136 | } 137 | if (event.removed.length > 0) { 138 | this.store.dispatch(new UpdateColumnsVisibilityAction({ 139 | columns: event.removed, 140 | tableName: event.tableName, 141 | isVisible: false, 142 | })); 143 | } 144 | } 145 | 146 | ngOnDestroy() { 147 | console.log('destroy'); 148 | this.ngUnsubscribe.next(); 149 | this.ngUnsubscribe.complete(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/app/table/table.data-mapping.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 |
10 | 21 | 22 | {{tmpSelectedItem[keyword_input.column.suggestion_column_name]}} 23 | tmpSelectedItem 24 | 25 | 26 | {{tmpItem[keyword_input.column.suggestion_column_name]}} tmpItem 27 | 28 | 29 |
30 | 31 |
32 | 44 | 45 | {{tmpSelectedItem[mapping_input.column.suggestion_column_name]}} 46 | tmpSelectedItem 47 | 48 | 49 | {{tmpItem[mapping_input.column.suggestion_column_name]}} tmpItem 50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /src/app/table/table.data-mapping.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { SuggestionsQuery } from './table.models'; 3 | 4 | @Component({ 5 | selector: 'app-table-data-mapping-component', 6 | templateUrl: './table.data-mapping.component.html', 7 | }) 8 | export class TableDataMappingComponent { 9 | public header = 'Transaction Mapping'; 10 | public keyword_input: any = { 11 | is_dropdown: false, 12 | is_multiple: false, 13 | custom_name: 'Keyword', 14 | data: null, 15 | column: 16 | { 17 | 'id': 124, 18 | 'can_update': false, 19 | 'column_name': 'description', 20 | 'custom_name': 'Description', 21 | 'data_type': 'text', 22 | 'filter_match_mode': 'contains', 23 | 'filter_value': null, 24 | 'format_pattern': null, 25 | 'input_type': 'text', 26 | 'is_filterable': false, 27 | 'is_multiple': false, 28 | 'is_select_item': false, 29 | 'is_sortable': true, 30 | 'is_visible': true, 31 | 'schema_name': 'banking', 32 | 'select_item_schema_name': null, 33 | 'select_item_table_name': null, 34 | 'select_item_label_column_name': null, 35 | 'select_item_value_column_name': null, 36 | 'suggestion_column_name': 'keyword', 37 | 'suggestion_schema_name': 'bookkeeping', 38 | 'suggestion_table_name': 'mappings', 39 | 'table_name': 'transactions', 40 | 'user_id': '1840e030-186e-4be3-a517-79762a96427d', 41 | 'styles': { 42 | 'height': 'auto', 43 | 'overflow': 'visible', 44 | 'padding-bottom': 'auto', 45 | 'padding-left': 'auto', 46 | 'padding-right': 'auto', 47 | 'padding-top': 'auto', 48 | 'width': '350px', 49 | }, 50 | }, 51 | style: { 52 | width: '100%' 53 | } 54 | }; 55 | 56 | public mapping_input: any = { 57 | is_dropdown: false, 58 | is_multiple: false, 59 | custom_name: 'Mapping', 60 | data: null, 61 | column: { 62 | 'id': 74, 63 | 'can_update': false, 64 | 'column_name': 'name', 65 | 'custom_name': 'Name', 66 | 'data_type': 'character varying', 67 | 'filter_match_mode': 'contains', 68 | 'filter_value': null, 69 | 'format_pattern': null, 70 | 'input_type': 'text', 71 | 'is_filterable': false, 72 | 'is_multiple': false, 73 | 'is_select_item': false, 74 | 'is_sortable': true, 75 | 'is_visible': true, 76 | 'schema_name': 'chart_of_accounts', 77 | 'select_item_schema_name': null, 78 | 'select_item_table_name': null, 79 | 'select_item_label_column_name': null, 80 | 'select_item_value_column_name': null, 81 | 'suggestion_column_name': 'name', 82 | 'suggestion_schema_name': 'chart_of_accounts', 83 | 'suggestion_table_name': 'expense_subaccounts', 84 | 'table_name': 'expense_subaccounts', 85 | 'user_id': '1840e030-186e-4be3-a517-79762a96427d', 86 | 'styles': { 87 | 'height': 'auto', 88 | 'overflow': 'visible', 89 | 'padding-bottom': 'auto', 90 | 'padding-left': 'auto', 91 | 'padding-right': 'auto', 92 | 'padding-top': 'auto', 93 | 'width': '200px' 94 | } 95 | }, 96 | style: { 97 | width: '100%' 98 | } 99 | }; 100 | 101 | public mapping_table: any = { 102 | 'schema_name': 'bookkeeping', 103 | 'table_name': 'mappings', 104 | 'keyword_column_name': 'keyword', 105 | 'mapping_column_name': 'negative_debit_subaccount_id' 106 | }; 107 | 108 | @Input() suggestions: string[]; 109 | 110 | @Output() getKeywordSuggestions = new EventEmitter(); 111 | @Output() getMappingSuggestions = new EventEmitter(); 112 | @Output() onEdit = new EventEmitter(); 113 | @Output() onEditCancel = new EventEmitter(); 114 | @Output() onEditComplete = new EventEmitter(); 115 | 116 | 117 | onCellEditorKeydown(event: any) { 118 | this.onEdit.emit({ 119 | originalEvent: event, 120 | // column: column, 121 | // data: rowData, 122 | // index: rowIndex, 123 | }); 124 | 125 | if (event.keyCode === 13) { // enter 126 | this.onEditComplete.emit({ 127 | // column: column, 128 | // data: rowData, 129 | // index: rowIndex, 130 | }); 131 | } else if (event.keyCode === 27) { // escape 132 | this.onEditCancel.emit({ 133 | // column: column, 134 | // data: rowData, 135 | // index: rowIndex, 136 | }); 137 | event.preventDefault(); 138 | } else if (event.keyCode === 9) { // tab 139 | this.onEditComplete.emit({ 140 | // column: column, 141 | // data: rowData, 142 | // index: rowIndex, 143 | }); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/app/table/table.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Action, Store } from '@ngrx/store'; 5 | import { Observable, of } from 'rxjs'; 6 | import {catchError, filter, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators'; 7 | 8 | import { 9 | ReceiveDatatableAction, 10 | ReceiveDatatableColumnsAction, 11 | ReceiveRecordsAction, 12 | AreRecordsLoadingAction, 13 | UpdateRowCountAction, 14 | GetRecordsAction, 15 | GetDatatableColumnsAction, GetDatatableAction, UpdatePaginationAction, 16 | UpdateSortAction, UpdateColumnsVisibilityAction, 17 | SelectTableAction, UpdateTableNameAction, 18 | SELECT_TABLE, GET_DATATABLE, GET_SUGGESTIONS, UPDATE_PAGINATION, UPDATE_SORT, 19 | GET_DATATABLE_COLUMNS, UPDATE_COLUMNS_VISIBILITY, GET_RECORDS, 20 | GetSuggestionsAction, ReceiveSuggestionsAction, UPDATE_RECORD, 21 | UpdateRecordAction, DELETE_RECORD, DeleteRecordAction, UPDATE_KEYWORD, 22 | UpdateKeywordAction, 23 | } from './table.actions'; 24 | import { TableService } from './table.service'; 25 | import { Datatable, DatatableColumn } from './table.models'; 26 | import { RouteParams } from '../router/router.models'; 27 | import { AppState } from '../app.reducers'; 28 | 29 | @Injectable() 30 | export class TableEffects { 31 | 32 | @Effect() 33 | deleteRecord$ = this.actions$.pipe( 34 | ofType(DELETE_RECORD), 35 | withLatestFrom(this.store), 36 | switchMap(([action, state]: [DeleteRecordAction, AppState]) => { 37 | return this.tableService.delete_record(action.payload).pipe( 38 | mergeMap(() => { 39 | return [ 40 | new GetRecordsAction(state.table.datatable)]; 41 | })) 42 | })); 43 | 44 | @Effect() 45 | selectTable$ = this.actions$.pipe( 46 | ofType(SELECT_TABLE), 47 | map((action: SelectTableAction) => action.payload), 48 | mergeMap((routeParams: RouteParams) => { 49 | return [ 50 | new GetDatatableAction(routeParams), 51 | new GetDatatableColumnsAction(routeParams), 52 | new UpdateTableNameAction(routeParams), 53 | ] 54 | })); 55 | 56 | @Effect() 57 | getDatatable$ = this.actions$.pipe( 58 | ofType(GET_DATATABLE), 59 | switchMap((action: GetDatatableAction) => { 60 | return this.tableService.get_datatable(action.payload.selectedSchemaName, 61 | action.payload.selectedObjectName).pipe( 62 | mergeMap((response: any) => { 63 | const datatable: Datatable = response.body[0]; 64 | return [ 65 | new ReceiveDatatableAction(datatable), 66 | new GetRecordsAction(datatable), 67 | ]; 68 | }), 69 | catchError((error: any) => { 70 | return of(new ReceiveDatatableAction(error)); 71 | })) 72 | })); 73 | 74 | @Effect() 75 | updatePagination$ = this.actions$.pipe( 76 | ofType(UPDATE_PAGINATION), 77 | switchMap((action: UpdatePaginationAction) => this.tableService.update_pagination(action.payload).pipe( 78 | mergeMap((response: any) => { 79 | const datatable: Datatable = response.body[0]; 80 | return [ 81 | new ReceiveDatatableAction(datatable), 82 | new GetRecordsAction(datatable), 83 | ]; 84 | }), 85 | catchError((error: any) => { 86 | return of(new ReceiveDatatableAction(error)); 87 | })), 88 | )); 89 | 90 | @Effect() 91 | updateKeyword$ = this.actions$.pipe( 92 | ofType(UPDATE_KEYWORD), 93 | switchMap((action: UpdateKeywordAction) => this.tableService.update_keyword(action.payload).pipe( 94 | mergeMap((response: any) => { 95 | const datatable: Datatable = response.body[0]; 96 | return [ 97 | new ReceiveDatatableAction(datatable), 98 | new GetRecordsAction(datatable), 99 | ]; 100 | }), 101 | catchError((error: any) => { 102 | return of(new ReceiveDatatableAction(error)); 103 | })), 104 | )); 105 | 106 | @Effect() 107 | updateSort$ = this.actions$.pipe( 108 | ofType(UPDATE_SORT), 109 | switchMap((action: UpdateSortAction) => this.tableService.update_sort(action.payload).pipe( 110 | mergeMap((response: any) => { 111 | console.log(response); 112 | const datatable: Datatable = response.body[0]; 113 | return [ 114 | new ReceiveDatatableAction(datatable), 115 | new GetRecordsAction(datatable), 116 | ]; 117 | }), 118 | catchError((error: any) => { 119 | return of(new ReceiveDatatableAction(error)); 120 | })), 121 | )); 122 | 123 | @Effect() 124 | getDatatableColumns$ = this.actions$.pipe( 125 | ofType(GET_DATATABLE_COLUMNS), 126 | switchMap((action: GetDatatableColumnsAction) => { 127 | return this.tableService.get_datatable_columns(action.payload.selectedSchemaName, 128 | action.payload.selectedObjectName).pipe( 129 | mergeMap((response: any) => { 130 | return [ 131 | new ReceiveDatatableColumnsAction(response.body), 132 | ]; 133 | }), 134 | catchError((error: any) => { 135 | return of(new ReceiveDatatableColumnsAction(error)); 136 | })) 137 | })); 138 | 139 | 140 | @Effect() 141 | updateColumnsVisibility$ = this.actions$.pipe( 142 | ofType(UPDATE_COLUMNS_VISIBILITY), 143 | switchMap((action: UpdateColumnsVisibilityAction) => this.tableService.update_columns_visibility(action.payload).pipe( 144 | mergeMap((response: any) => { 145 | return [ 146 | new GetDatatableColumnsAction(response.body[0].table_name), 147 | ]; 148 | }), 149 | catchError(() => { 150 | return of(new GetDatatableColumnsAction(action.payload.dataTable)); 151 | })), 152 | )); 153 | 154 | @Effect() 155 | getRecords$: Observable = this.actions$.pipe( 156 | ofType(GET_RECORDS), 157 | switchMap((action: GetRecordsAction) => this.tableService.get_records(action.payload).pipe( 158 | mergeMap((response: any) => { 159 | const rowCountString = response.headers!.get('content-range')!.split('/')[1]; 160 | const rowCount = parseInt(rowCountString, 10); 161 | return [ 162 | new ReceiveRecordsAction(response.body), 163 | new UpdateRowCountAction(rowCount), 164 | new AreRecordsLoadingAction(false), 165 | ]; 166 | }), 167 | catchError((error: any) => { 168 | return of(new ReceiveRecordsAction(error)); 169 | })))); 170 | 171 | @Effect() 172 | getKeywordRecords$: Observable = this.actions$.pipe( 173 | ofType(UPDATE_KEYWORD), 174 | withLatestFrom(this.store), 175 | switchMap(([action, state]: [UpdateKeywordAction, AppState]) => { 176 | const datatable: Datatable = state.table.datatable; 177 | const column: DatatableColumn = action.payload.column; 178 | column.filter_value = action.payload.value; 179 | datatable.filter_columns = [action.payload.column]; 180 | return this.tableService.get_records(datatable).pipe( 181 | mergeMap((response: any) => { 182 | const rowCountString = response.headers!.get('content-range')!.split('/')[1]; 183 | const rowCount = parseInt(rowCountString, 10); 184 | return [ 185 | new ReceiveRecordsAction(response.body), 186 | new UpdateRowCountAction(rowCount), 187 | new AreRecordsLoadingAction(false), 188 | ]; 189 | }), 190 | catchError((error: any) => { 191 | return of(new ReceiveRecordsAction(error)); 192 | })) 193 | } 194 | )); 195 | 196 | @Effect() 197 | getSelectItems$ = this.actions$.pipe( 198 | ofType(GET_SUGGESTIONS), 199 | switchMap((action: GetSuggestionsAction) => { 200 | console.log(action.payload); 201 | return this.tableService.get_suggestions(action.payload) 202 | .mergeMap((response: any) => { 203 | return [ 204 | new ReceiveSuggestionsAction(response.body), 205 | ]; 206 | }) 207 | })); 208 | 209 | 210 | @Effect() 211 | updateRecord$ = this.actions$.pipe( 212 | ofType(UPDATE_RECORD), 213 | withLatestFrom(this.store), 214 | filter(([action, state]: [UpdateRecordAction, AppState]) => { 215 | // Make sure the value actually changed 216 | return action.payload.value !== state.table.records.filter(r => r.id === action.payload.record_id)[0][action.payload.column_name] 217 | }), 218 | switchMap(([action, state]: [UpdateRecordAction, AppState]) => { 219 | return this.tableService.update_record(action.payload) 220 | .mergeMap((): any[] => []) 221 | .catch((error: any) => { 222 | return of(new ReceiveDatatableAction(error)); 223 | }) 224 | }, 225 | )); 226 | 227 | constructor(private actions$: Actions, 228 | private store: Store, 229 | private tableService: TableService) { 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/app/table/table.models.ts: -------------------------------------------------------------------------------- 1 | import { Column, LazyLoadEvent } from 'primeng/primeng'; 2 | 3 | export interface ColumnResizeEvent { 4 | element: HTMLElement; 5 | delta: number; 6 | } 7 | 8 | export interface ColumnsVisibilityUpdate { 9 | columns: string[]; 10 | isVisible: boolean; 11 | schemaName: string; 12 | tableName: string; 13 | } 14 | 15 | export interface EditEvent { 16 | column: Column; 17 | data: any; 18 | index: number; 19 | } 20 | 21 | export interface Datatable { 22 | can_archive: boolean; 23 | can_delete: boolean; 24 | custom_name: string; 25 | order_index: number; 26 | row_limit: number; 27 | row_offset: number; 28 | schema_name: string; 29 | sort_column: string; 30 | sort_order: number; 31 | table_name: string; 32 | user_id: string; 33 | filter_columns: Array; 34 | } 35 | 36 | export interface DatatableColumn { 37 | can_update: boolean; 38 | column_name: string; 39 | custom_name: string; 40 | data_type: string; 41 | filter_match_mode: string; 42 | filter_value: string; 43 | format_pattern: string; 44 | input_type: string; 45 | is_filterable: boolean; 46 | is_multiple: boolean; 47 | is_select_item: boolean; 48 | is_sortable: boolean; 49 | is_visible: boolean; 50 | schema_name: string; 51 | select_item_label_column_name: string; 52 | select_item_schema_name: string; 53 | select_item_table_name: string; 54 | select_item_value_column_name: string; 55 | styles: Styles; 56 | suggestion_column_name: string; 57 | suggestion_schema_name: string; 58 | suggestion_table_name: string; 59 | table_name: string; 60 | user_id: string; 61 | } 62 | 63 | export interface DatatableUpdate extends LazyLoadEvent { 64 | schemaName: string; 65 | tableName: string; 66 | } 67 | 68 | export interface MultiselectOutput { 69 | added: DatatableColumn[]; 70 | removed: DatatableColumn[]; 71 | schemaName?: string; 72 | tableName?: string; 73 | } 74 | 75 | export interface DeleteRecord { 76 | record_id: any; 77 | table_name: string; 78 | schema_name: string; 79 | } 80 | 81 | export interface UpdateRecord { 82 | value: any; 83 | record_id: any; 84 | column_name: string; 85 | table_name: string; 86 | schema_name: string; 87 | } 88 | 89 | export interface Styles { 90 | height: string; 91 | overflow: string; 92 | 'padding-bottom': string; 93 | 'padding-left': string; 94 | 'padding-right': string; 95 | 'padding-top': string; 96 | width: string; 97 | } 98 | 99 | export interface SuggestionsQuery { 100 | column: DatatableColumn; 101 | value: string; 102 | } 103 | -------------------------------------------------------------------------------- /src/app/table/table.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { AutoCompleteModule } from 'primeng/components/autocomplete/autocomplete'; 6 | import { ButtonModule } from 'primeng/components/button/button'; 7 | import { DataTableModule } from 'primeng/components/datatable/datatable'; 8 | import { FieldsetModule } from 'primeng/components/fieldset/fieldset'; 9 | import { InputTextModule } from 'primeng/components/inputtext/inputtext'; 10 | import { MultiSelectModule } from 'primeng/components/multiselect/multiselect'; 11 | import { PasswordModule } from 'primeng/components/password/password'; 12 | 13 | import { TableContainer } from './table.container'; 14 | import { TableComponent } from './table.component'; 15 | import { TableService } from './table.service'; 16 | import { TableDataMappingComponent } from './table.data-mapping.component'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | AutoCompleteModule, 21 | ButtonModule, 22 | CommonModule, 23 | DataTableModule, 24 | FieldsetModule, 25 | FormsModule, 26 | InputTextModule, 27 | MultiSelectModule, 28 | PasswordModule, 29 | ReactiveFormsModule, 30 | ], 31 | declarations: [ 32 | TableComponent, 33 | TableContainer, 34 | TableDataMappingComponent 35 | ], 36 | providers: [ 37 | TableService, 38 | ], 39 | exports: [ 40 | TableContainer, 41 | ] 42 | }) 43 | export class AppTableModule { 44 | } 45 | -------------------------------------------------------------------------------- /src/app/table/table.reducers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_RECORD, 3 | ARE_RECORDS_LOADING, 4 | DESELECT_RECORDS, 5 | DESELECT_RECORD, 6 | GET_DATATABLE, 7 | GET_DATATABLE_COLUMNS, 8 | GET_SUGGESTIONS, 9 | RECEIVE_DATATABLE, 10 | RECEIVE_DATATABLE_COLUMNS, 11 | RECEIVE_RECORDS, 12 | REMOVE_RECORD, 13 | REMOVE_RECORDS, 14 | SELECT_RECORDS, 15 | TableActions, 16 | UPDATE_ROW_COUNT, 17 | UPDATE_TABLE_NAME, RECEIVE_SUGGESTIONS, UPDATE_KEYWORD, 18 | } from './table.actions'; 19 | import { Datatable, DatatableColumn } from './table.models'; 20 | 21 | 22 | export interface TableState { 23 | areColumnsLoading: boolean; 24 | areRecordsLoading: boolean; 25 | columns: DatatableColumn[]; 26 | datatable: Datatable | null; 27 | isDatatableLoading: boolean; 28 | records: any[]; 29 | rowCount: number | null; 30 | rowLimit: number | null; 31 | rowOffset: number | null; 32 | schemaName: string | null; 33 | selectedRecords: any[]; 34 | suggestions: string[]; 35 | suggestionsColumn: DatatableColumn | null; 36 | sortColumn: string | null; 37 | sortOrder: number | null; 38 | tableName: string | null; 39 | } 40 | 41 | const initialState: TableState = { 42 | areColumnsLoading: true, 43 | areRecordsLoading: true, 44 | columns: [], 45 | datatable: null, 46 | isDatatableLoading: true, 47 | records: [], 48 | rowCount: null, 49 | rowLimit: null, 50 | rowOffset: 0, 51 | schemaName: null, 52 | selectedRecords: [], 53 | suggestions: [], 54 | suggestionsColumn: null, 55 | sortColumn: null, 56 | sortOrder: 1, 57 | tableName: null, 58 | }; 59 | 60 | export function tableReducer(state = initialState, action: TableActions): TableState { 61 | console.log(action); 62 | switch (action.type) { 63 | case ADD_RECORD: 64 | return Object.assign({}, state, { 65 | records: [action.payload, ...state.records], 66 | }); 67 | case ARE_RECORDS_LOADING: 68 | return Object.assign({}, state, { 69 | tableRecordsAreLoading: action.payload, 70 | }); 71 | case DESELECT_RECORD: 72 | return Object.assign({}, state, { 73 | selectedRecords: state.selectedRecords.filter(record => record.id !== action.payload.id), 74 | }); 75 | case DESELECT_RECORDS: 76 | return Object.assign({}, state, { 77 | selectedRecords: [], 78 | }); 79 | case GET_DATATABLE: 80 | return Object.assign({}, state, { 81 | isDatatableLoading: true, 82 | datatable: null, 83 | records: [], 84 | }); 85 | case GET_DATATABLE_COLUMNS: 86 | return Object.assign({}, state, { 87 | areColumnsLoading: true, 88 | records: [], 89 | columns: [], 90 | }); 91 | case GET_SUGGESTIONS: 92 | return Object.assign({}, state, { 93 | suggestionsColumn: action.payload.column, 94 | }); 95 | case RECEIVE_DATATABLE: 96 | return Object.assign({}, state, { 97 | datatable: action.payload, 98 | isDatatableLoading: false, 99 | rowLimit: action.payload.row_limit, 100 | rowOffset: action.payload.row_offset, 101 | sortColumn: action.payload.sort_column, 102 | sortOrder: action.payload.sort_order, 103 | }); 104 | case RECEIVE_DATATABLE_COLUMNS: 105 | return Object.assign({}, state, { 106 | areColumnsLoading: false, 107 | columns: action.payload, 108 | }); 109 | case RECEIVE_RECORDS: 110 | return Object.assign({}, state, { 111 | records: action.payload, 112 | areRecordsLoading: false, 113 | }); 114 | case RECEIVE_SUGGESTIONS: 115 | return Object.assign({}, state, { 116 | suggestions: action.payload, 117 | }); 118 | case REMOVE_RECORD: 119 | return Object.assign({}, state, { 120 | records: state.records.filter(record => record.id !== action.payload.id), 121 | }); 122 | case REMOVE_RECORDS: 123 | return Object.assign({}, state, { 124 | records: [], 125 | }); 126 | case SELECT_RECORDS: 127 | return Object.assign({}, state, { 128 | selectedRecords: action.payload, 129 | }); 130 | case UPDATE_KEYWORD: 131 | console.log(action.payload); 132 | return Object.assign({}, state, { 133 | datatable: Object.assign({}, state.datatable, { 134 | filter_columns: [action.payload.column], 135 | }), 136 | }); 137 | case UPDATE_ROW_COUNT: 138 | return Object.assign({}, state, { 139 | rowCount: action.payload, 140 | }); 141 | case UPDATE_TABLE_NAME: 142 | return Object.assign({}, state, { 143 | tableName: action.payload.selectedObjectName, 144 | schemaName: action.payload.selectedSchemaName, 145 | }); 146 | default: 147 | return state; 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /src/app/table/table.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpParams } from '@angular/common/http'; 3 | 4 | import { Observable } from 'rxjs'; 5 | 6 | import { Store } from '@ngrx/store'; 7 | 8 | import { AppState } from '../app.reducers'; 9 | import { RestClient } from 'app/rest/rest.service'; 10 | 11 | import { AreRecordsLoadingAction } from './table.actions'; 12 | import { 13 | ColumnsVisibilityUpdate, Datatable, DatatableUpdate, 14 | UpdateRecord, SuggestionsQuery, DeleteRecord, 15 | } from './table.models'; 16 | 17 | @Injectable() 18 | export class TableService { 19 | get_datatable(schemaName: string, tableName: string): Observable { 20 | let params: HttpParams = new HttpParams(); 21 | params = params.set('table_name', 'eq.' + tableName); 22 | params = params.set('schema_name', 'eq.' + schemaName); 23 | return this.restClient.get('admin', '/datatables', params) 24 | }; 25 | 26 | get_datatable_columns(schemaName: string, tableName: string): Observable { 27 | let params: HttpParams = new HttpParams(); 28 | params = params.set('table_name', 'eq.' + tableName); 29 | params = params.set('schema_name', 'eq.' + schemaName); 30 | return this.restClient.get('admin', '/datatable_columns', params); 31 | }; 32 | 33 | get_records(datatable: Datatable): Observable { 34 | let params: HttpParams = new HttpParams(); 35 | let sortDirection: string; 36 | params = params.set('limit', datatable.row_limit.toString()); 37 | params = params.set('offset', datatable.row_offset.toString()); 38 | if (datatable.sort_column !== null) { 39 | sortDirection = datatable.sort_order === 1 ? 'asc' : 'desc'; 40 | params = params.set('order', datatable.sort_column + '.' + sortDirection); 41 | } 42 | console.log(datatable); 43 | datatable.filter_columns.map(column => { 44 | if (column.filter_value !== null) { 45 | params = params.set(column.column_name, 'ilike.*' + column.filter_value + '*'); 46 | } 47 | }, 48 | ); 49 | this.store.dispatch(new AreRecordsLoadingAction(true)); 50 | 51 | return this.restClient.get(datatable.schema_name, '/' + datatable.table_name, params); 52 | }; 53 | 54 | get_suggestions(query: SuggestionsQuery): Observable { 55 | let params: HttpParams = new HttpParams(); 56 | const endpointName = '/' + query.column.suggestion_table_name; 57 | params = params.set('select', [query.column.suggestion_column_name, 'id'].join(',')); 58 | params = params.set('order', query.column.suggestion_column_name); 59 | params = params.set('limit', '10'); 60 | params = params.set(query.column.suggestion_column_name, 'ilike.*' + query.value + '*'); 61 | return this.restClient.get(query.column.suggestion_schema_name, endpointName, params) 62 | } 63 | 64 | delete_record(deleteRecord: DeleteRecord): Observable { 65 | return this.restClient.delete(deleteRecord.schema_name, '/' + deleteRecord.table_name, deleteRecord.record_id) 66 | }; 67 | 68 | update_columns_visibility(updateData: ColumnsVisibilityUpdate): Observable { 69 | const data = { 70 | is_visible: updateData.isVisible, 71 | }; 72 | let params: HttpParams = new HttpParams(); 73 | params = params.set('table_name', 'eq.' + updateData.tableName); 74 | params = params.set('value', 'in.' + updateData.columns.join(',')); 75 | return this.restClient.patch('admin', '/datatable_columns', data, params) 76 | }; 77 | 78 | update_keyword(updateData: any): Observable { 79 | const data = { 80 | filter_columns: [updateData.column], 81 | }; 82 | let params: HttpParams = new HttpParams(); 83 | params = params.set('table_name', 'eq.' + updateData.column.table_name); 84 | params = params.set('schema_name', 'eq.' + updateData.column.schema_name); 85 | return this.restClient.patch('admin', '/datatables', data, params) 86 | } 87 | 88 | update_pagination(updateData: DatatableUpdate): Observable { 89 | const newOffset = updateData.first; 90 | const newLimit = updateData.rows; 91 | const data = { 92 | row_offset: newOffset, 93 | row_limit: newLimit, 94 | }; 95 | let params: HttpParams = new HttpParams(); 96 | params = params.set('table_name', 'eq.' + updateData.tableName); 97 | return this.restClient.patch('admin', '/datatables', data, params) 98 | }; 99 | 100 | update_record(updateData: UpdateRecord): Observable { 101 | let params: HttpParams = new HttpParams(); 102 | const data: any = {}; 103 | data[updateData['column_name']] = updateData.value; 104 | params = params.set('id', 'eq.' + updateData.record_id); 105 | return this.restClient.patch(updateData.schema_name, '/' + updateData.table_name, data, params) 106 | }; 107 | 108 | update_sort(updateData: DatatableUpdate): Observable { 109 | const data = { 110 | sort_column: updateData.sortField, 111 | sort_order: updateData.sortOrder, 112 | }; 113 | let params: HttpParams = new HttpParams(); 114 | params = params.set('table_name', 'eq.' + updateData.tableName); 115 | return this.restClient.patch('admin', '/datatables', data, params) 116 | } 117 | 118 | 119 | constructor(private restClient: RestClient, 120 | private store: Store) { 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/app/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function coerces a string into a string literal type. 3 | * Using tagged union types in TypeScript 2.0, this enables 4 | * powerful typechecking of our reducers. 5 | * 6 | * Since every action label passes through this function it 7 | * is a good place to ensure all of our action labels 8 | * are unique. 9 | */ 10 | 11 | const typeCache: { [label: string]: boolean } = {}; 12 | export function type(label: T | ''): T { 13 | if (typeCache[label]) { 14 | throw new Error(`Action type "${label}" is not unique"`); 15 | } 16 | 17 | typeCache[label] = true; 18 | 19 | return label; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/websocket/websocket.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { type } from '../util'; 4 | 5 | export const WebsocketActionTypes = { 6 | CONNECT: type('CONNECT'), 7 | }; 8 | 9 | export class ConnectAction implements Action { 10 | type = WebsocketActionTypes.CONNECT; 11 | constructor(public payload: string) {} 12 | } 13 | 14 | export type WebsocketActions 15 | = ConnectAction; 16 | -------------------------------------------------------------------------------- /src/app/websocket/websocket.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect } from '@ngrx/effects'; 4 | 5 | import * as websocket from './websocket.actions'; 6 | import * as table from '../table/table.actions'; 7 | 8 | import * as fromRoot from '../app.reducers'; 9 | 10 | import { $WebSocket } from 'angular2-websocket/angular2-websocket'; 11 | import { Store } from '@ngrx/store'; 12 | import { WebsocketService } from './websocket.service'; 13 | 14 | @Injectable() 15 | export class WebsocketEffects { 16 | private ws: $WebSocket; 17 | 18 | constructor(private actions$: Actions, 19 | private websocket$: WebsocketService, 20 | private store: Store,) { 21 | } 22 | 23 | // @Effect() 24 | // connect$ = this.actions$ 25 | // .ofType(websocket.ActionTypes.CONNECT) 26 | // .switchMap(action => [this.websocket$.connect('ws://localhost:4545/' + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2RlIjoicnciLCJjaGFubmVsIjoibWVzc2FnZXNfdGFibGVfdXBkYXRlIn0.zZDpPodeBkvAfpmhq9YMLCAAzWk5WzlUwb9oa9M_Rvk')] 27 | // ) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/websocket/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import {$WebSocket} from 'angular2-websocket/angular2-websocket'; 2 | import {Injectable} from '@angular/core'; 3 | import {Store} from '@ngrx/store'; 4 | 5 | import {AppState} from '../app.reducers'; 6 | import {AddRecordAction, DeselectRecordAction, RemoveRecordAction} from '../table/table.actions'; 7 | 8 | 9 | @Injectable() 10 | export class WebsocketService { 11 | private ws: $WebSocket; 12 | 13 | constructor(private store: Store) { 14 | // this.store.select(getRecords); 15 | }; 16 | 17 | connect(url: string) { 18 | this.ws = new $WebSocket(url); 19 | return this.ws.getDataStream() 20 | .subscribe( 21 | (response: any) => { 22 | const message = JSON.parse(response.data); 23 | switch (message.type) { 24 | case 'INSERT': 25 | this.store.dispatch(new AddRecordAction(message.row)); 26 | break; 27 | case 'DELETE': 28 | this.store.dispatch(new RemoveRecordAction(message.row)); 29 | this.store.dispatch(new DeselectRecordAction(message.row)); 30 | break; 31 | default: 32 | break; 33 | } 34 | }, 35 | (error: any) => console.log('Error: ' + error.message), 36 | () => console.log('Completed'), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PierreRochard/general-angular/1818d4b47795fc49620b328fb59169f5a0243a66/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PierreRochard/general-angular/1818d4b47795fc49620b328fb59169f5a0243a66/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PostgREST Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import 'rxjs/add/operator/catch'; 8 | import 'rxjs/add/observable/combineLatest'; 9 | import 'rxjs/add/operator/do'; 10 | import 'rxjs/add/operator/filter'; 11 | import 'rxjs/add/operator/map'; 12 | import 'rxjs/add/operator/mergeMap'; 13 | import 'rxjs/add/observable/of'; 14 | import 'rxjs/add/operator/take'; 15 | import 'rxjs/add/operator/takeUntil'; 16 | import 'rxjs/add/operator/timeout'; 17 | import 'rxjs/add/operator/withLatestFrom'; 18 | import 'rxjs/add/operator/switchMap'; 19 | 20 | 21 | if (environment.production) { 22 | enableProdMode(); 23 | } 24 | 25 | platformBrowserDynamic().bootstrapModule(AppModule); 26 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular and is loaded before the app. 2 | // You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | 21 | // If you need to support the browsers/features below, uncomment the import 22 | // and run `npm install import-name-here'; 23 | // Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 24 | 25 | // Needed for: IE9 26 | // import 'classlist.js'; 27 | 28 | // Animations 29 | // Needed for: All but Chrome and Firefox, Not supported in IE9 30 | // import 'web-animations-js'; 31 | 32 | // Date, currency, decimal and percent pipes 33 | // Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 34 | // import 'intl'; 35 | 36 | // NgClass on SVG elements 37 | // Needed for: IE10, IE11 38 | // import 'classlist.js'; 39 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url('../node_modules/primeng/resources/themes/omega/theme.css'); 3 | @import url('../node_modules/primeng/resources/primeng.min.css'); 4 | @import url('../node_modules/font-awesome/css/font-awesome.min.css'); 5 | -------------------------------------------------------------------------------- /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 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "baseUrl": "", 7 | "declaration": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "lib": ["es7", "dom"], 12 | "mapRoot": "./", 13 | "module": "es6", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": false, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "outDir": "../dist/out-tsc", 22 | "pretty": true, 23 | "sourceMap": true, 24 | "strictNullChecks": false, 25 | "target": "es5", 26 | "typeRoots": [ 27 | "../node_modules/@types" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": true, 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": false, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /uml/component_architecture.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | title Component Architecture 3 | 4 | package App <> { 5 | class AppComponent 6 | class MenubarComponent 7 | 8 | } 9 | 10 | @enduml 11 | -------------------------------------------------------------------------------- /uml/store_architecture.puml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PierreRochard/general-angular/1818d4b47795fc49620b328fb59169f5a0243a66/uml/store_architecture.puml --------------------------------------------------------------------------------